Compare commits

...

79 Commits

Author SHA1 Message Date
b3de2e5d6b Merge pull request #1303 from ImranR98/dev
- Better partial APK hash as pseudo-version (related to #1101)
- Support Direct APK Links (#1292)
- Easier to understand version detection settings
- Bugfix in automatic source selection for URL input
- Various UI improvements and wording changes
- Add invert option to apk filter regex 
- Filter out fdroid APKs from default Obtainium entry (for later)
2024-01-19 23:41:27 -05:00
0183070b2b Merge remote-tracking branch 'origin/main' into dev 2024-01-19 23:38:23 -05:00
089bbc3ee9 Merge pull request #1291 from Bardesss/patch-1
Update nl.json
2024-01-19 23:29:49 -05:00
b085801c2c Merge pull request #1298 from jont4/main
Translation: Some fixes and updates in pt.json
2024-01-19 23:29:40 -05:00
ad4cae4288 Add F-Droid build to GitHub action 2024-01-19 23:29:22 -05:00
95c285e1f7 Filter out fdroid APKs from default Obtainium entry 2024-01-19 23:25:00 -05:00
c31a1912a5 Add invert option to apk filter regex 2024-01-19 23:22:00 -05:00
99da1f8481 Upgrade packages, increment version 2024-01-19 20:41:07 -05:00
bf6b3a7da0 UI bug 2024-01-19 20:36:40 -05:00
4ab4f58f65 Cleaner 'add app' page 2024-01-19 20:35:44 -05:00
29a76ac8e6 Removed unused divider and commented code on settings page 2024-01-19 19:59:30 -05:00
e2761ce284 UI improvements on app page 2024-01-19 19:57:28 -05:00
dc45334cb9 Support Direct APK Links (#1292) 2024-01-19 18:54:01 -05:00
56052bfd79 Bugfix in version detection toggles 2024-01-19 00:43:02 -05:00
f5067d636a More UI tweaks 2024-01-19 00:21:57 -05:00
b173b1300a Clearer wording 2024-01-19 00:12:43 -05:00
15a34e53bf Bugfix in automatic source selection for URL input 2024-01-19 00:00:47 -05:00
183fb26988 Clearer version detection settings 2024-01-18 23:46:23 -05:00
2da9a8cd59 Remove unnecessary print() 2024-01-18 20:45:34 -05:00
bb07000280 Better partial APK hash as pseudo-version (related to #1101) 2024-01-18 20:45:06 -05:00
3a46ad2957 Some fixes and updates 2024-01-16 15:48:59 -03:00
6b631d119c Merge branch 'ImranR98:main' into main 2024-01-16 15:24:02 -03:00
051762fcc1 Update nl.json
Added new strings.
2024-01-15 11:38:00 +00:00
595d5dc283 Merge pull request #1289 from ImranR98/dev
Added badge graphic (#1287)
2024-01-14 15:14:41 -05:00
1d328f07e2 Merge remote-tracking branch 'origin/main' into dev 2024-01-14 15:14:01 -05:00
b1c8ac6f2a Added badge graphic (#1287) 2024-01-14 15:09:16 -05:00
da619d37f7 Merge pull request #1288 from iDazai/main
Update de.json
2024-01-14 14:56:54 -05:00
236c4722e5 Update de.json
added 2 missing strings, translated new strings
2024-01-14 15:36:08 +01:00
92f59116a0 Merge remote-tracking branch 'origin/main' into dev 2024-01-14 01:37:17 -05:00
c28040f0cb Merge remote-tracking branch 'origin/main' into dev 2024-01-14 01:36:43 -05:00
e8b9654320 Update release.yml 2024-01-14 01:36:35 -05:00
be1a793a37 Update release.yml 2024-01-14 01:34:32 -05:00
440720afb6 Update release.yml 2024-01-14 01:31:42 -05:00
900d3e734e Update release.yml 2024-01-14 01:27:13 -05:00
0bf096abb5 Temporary GitHub action change 2024-01-14 01:26:17 -05:00
632efb9e22 Merge remote-tracking branch 'origin/main' into dev 2024-01-14 01:24:50 -05:00
bfc506e450 Merge pull request #1285 from ImranR98/dev
- Improve loading time (parallelize version detection)
- Newest asset upload date as release date for GitHub (#1284)
- Enable 'start on boot' for BG task (#1234)
- Remove the need to hardcode Obtainium's version number
2024-01-14 01:24:25 -05:00
ffe612708c Remove the need to hardcode Obtainium's version number 2024-01-14 01:22:35 -05:00
5d161160aa Increment version 2024-01-14 00:56:25 -05:00
eadf3e5a29 Enable 'start on boot' for BG task (#1234) 2024-01-14 00:22:25 -05:00
6b6b4084a0 Newest asset upload date as release date for GitHub (#1284) 2024-01-14 00:14:44 -05:00
30a4633f72 Improve loading time (parallelize version detection) 2024-01-13 23:26:57 -05:00
d44139dd72 Merge pull request #1281 from ImranR98/dev
Allow entire app config to be shared as link (see https://github.com/ImranR98/apps.obtainium.imranr.dev/issues/5)
2024-01-12 23:47:31 -05:00
60869a0490 Allow entire app config to be shared as link (see https://github.com/ImranR98/apps.obtainium.imranr.dev/issues/5) 2024-01-12 23:46:50 -05:00
102be5fa2e Merge pull request #1280 from ImranR98/dev
- Fix form input issues (related to #1272)
- HTML Source bugfix (related to #1259)
2024-01-12 22:32:49 -05:00
a25c04b390 Update packages, increment version, dart fix 2024-01-12 22:32:33 -05:00
d46f0a1c33 HTML Source bugfix (related to #1259) 2024-01-12 22:16:06 -05:00
5c7f5a99e1 Attempt to fix form input issues for recursive forms (NOT FULLY TESTED) 2024-01-12 21:51:48 -05:00
d949a80f19 Merge pull request #1279 from gidano/main
Update hu.json
2024-01-12 20:29:33 -05:00
6b5d4e2027 Merge pull request #1278 from mehdeej/main
Update fa.json
2024-01-12 20:29:25 -05:00
199d071ee8 Update hu.json 2024-01-12 15:45:26 +01:00
5b78de5d61 Merge branch 'ImranR98:main' into main 2024-01-12 05:46:33 -03:00
db4f13e2e1 Update fa.json 2024-01-12 07:32:52 +03:30
5e869b7e03 Update release.yml 2024-01-11 21:58:48 -05:00
34fee36132 Merge pull request #1277 from ImranR98/dev
- Fix APKPure downloads (#1250)
- F-Droid bugfix (#1264)
- Allow versionCode to be used as the version for comparison (#1269)
- Add custom header support to HTML (#709, #1272)
- Disabled Mullvad source (#1276)
- Bugfixes: Don't make URLs lowercase, never auto-select Jenkins
2024-01-11 21:44:36 -05:00
ea09bc36a5 Merge branch 'main' into dev 2024-01-11 21:44:24 -05:00
f7e0678cb2 Remove unused variable 2024-01-11 21:39:41 -05:00
223ae378a9 Update packages, increment version 2024-01-11 21:39:26 -05:00
6f52a48991 Merge remote-tracking branch 'origin/main' into dev 2024-01-11 21:36:15 -05:00
0280935955 Merge pull request #1268 from bluefly000/japanese-translation
Update ja.json
2024-01-11 21:36:03 -05:00
eebc7d9ab3 Merge pull request #1271 from pmtpro/main
update vi.json
2024-01-11 21:35:53 -05:00
aa7b5652ff Fix APKPure downloads (#1250) 2024-01-11 21:34:46 -05:00
ec6683a198 Bugfixes: Don't make URLs lowercase, never auto-select Jenkins 2024-01-11 21:28:15 -05:00
05372924a0 F-Droid bugfix (#1264) 2024-01-11 21:16:25 -05:00
e63c1399dd Allow versionCode to be used as the version for comparison (#1269) 2024-01-11 20:10:20 -05:00
4fcad92e1d Disabled Mullvad source (#1276) 2024-01-11 19:50:51 -05:00
e18e7298bc GeneratedFormSubForm UI improvements 2024-01-11 19:43:17 -05:00
76a6a509cd Header usage bugfix 2024-01-11 19:23:59 -05:00
7a03561ff6 Add custom header support to HTML (#709, #1272) 2024-01-11 19:01:17 -05:00
70e54ce14a F-Droid small fix 2024-01-10 18:51:22 -05:00
b5f86f0e79 update vi.json 2024-01-10 10:20:59 +07:00
4ebca49ef7 Update ja.json 2024-01-09 22:26:11 +09:00
49cfa95463 Merge pull request #1265 from ImranR98/dev
- Multi-host support + add '.net' host to APKPure source (#1250)
- HTML link parsing bugfix (#1259)
- APKMirror version extraction bugfix (#1264)
2024-01-08 19:28:31 -05:00
4600ab0593 Increment version 2024-01-08 19:20:08 -05:00
7f2ca98bde Multi-host support + add '.net' host to APKPure source (#1250) 2024-01-08 19:17:50 -05:00
6511485bcf APKMirror version extraction bugfix (#1264) 2024-01-08 18:33:24 -05:00
fd22113e44 HTML link parsing bugfix (#1259) 2024-01-08 18:18:57 -05:00
f2ae078723 Merge branch 'ImranR98:main' into main 2024-01-08 18:20:27 -03:00
c3dacf2b9b Translation: tiny bug 2024-01-08 18:18:57 -03:00
58 changed files with 1556 additions and 902 deletions

View File

@ -2,6 +2,10 @@ name: Build and Release
on:
workflow_dispatch:
inputs:
beta:
type: boolean
description: Is beta?
jobs:
build:
@ -19,11 +23,23 @@ jobs:
gpg_private_key: ${{ secrets.PGP_KEY_BASE64 }}
passphrase: ${{ secrets.PGP_PASSPHRASE }}
- name: Extract Version
id: extract_version
run: |
VERSION=$(grep -oP "^version: [^\+]+" pubspec.yaml | tail -c +10)
echo "version=$VERSION" >> $GITHUB_OUTPUT
if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi
echo "beta=$BETA" >> $GITHUB_OUTPUT
TAG="v$VERSION"
if [ $BETA == true ]; then TAG="$TAG"-beta; fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Build APKs
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
flutter build apk --flavor fdroid -t lib/main_fdroid.dart && flutter build apk --split-per-abi --flavor fdroid -t lib/main_fdroid.dart
rm ./build/app/outputs/flutter-apk/*.sha1
ls -l ./build/app/outputs/flutter-apk/
@ -37,22 +53,12 @@ jobs:
for apk in ./build/app/outputs/flutter-apk/*-release*.apk; do
unsignedFn=${apk/-release/-unsigned}
mv "$apk" "$unsignedFn"
${ANDROID_HOME}/build-tools/30.0.2/apksigner sign --ks apksign.keystore --ks-pass pass:"${KEYSTORE_PASSWORD}" --out "${apk}" "${unsignedFn}"
${ANDROID_HOME}/build-tools/$(ls ${ANDROID_HOME}/build-tools/ | tail -1)/apksigner sign --ks apksign.keystore --ks-pass pass:"${KEYSTORE_PASSWORD}" --out "${apk}" "${unsignedFn}"
sha256sum ${apk} | cut -d " " -f 1 > "$apk".sha256
gpg --batch --pinentry-mode loopback --passphrase "${PGP_PASSPHRASE}" --sign --detach-sig "$apk".sha256
done
rm apksign.keystore
PGP_KEY_FINGERPRINT="${{ steps.import_pgp_key.outputs.fingerprint }}"
- name: Extract Version
id: extract_version
run: |
VERSION=$(grep -oP "currentVersion = '\K[^']+" lib/main.dart)
echo "version=$VERSION" >> $GITHUB_OUTPUT
TAG=$(grep -oP "'.*\\\$currentVersion.*'" lib/main.dart | head -c -2 | tail -c +2 | sed "s/\$currentVersion/$VERSION/g")
echo "tag=$TAG" >> $GITHUB_OUTPUT
if [ -n "$(echo $TAG | grep -oP '\-beta$')" ]; then BETA=true; else BETA=false; fi
echo "beta=$BETA" >> $GITHUB_OUTPUT
- name: Create Tag
uses: mathieudutour/github-tag-action@v6.1

View File

@ -21,7 +21,7 @@ Currently supported App sources:
- [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/)
- Other - General:
- [APKPure](https://apkpure.com/)
- [APKPure](https://apkpure.net/)
- [Aptoide](https://aptoide.com/)
- [Uptodown](https://uptodown.com/)
- [APKMirror](https://apkmirror.com/) (Track-Only)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -51,9 +51,8 @@
"percentProgress": "Napredak: {}%",
"pleaseWait": "Molimo sačekajte",
"updateAvailable": "Ažuriranje dostupno",
"estimateInBracketsShort": "(Procjena)",
"notInstalled": "Nije instalirano",
"estimateInBrackets": "(Procjena)",
"pseudoVersion": "pseudo-version",
"selectAll": "Označi sve",
"deselectX": "Poništi odabir {}",
"xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.",
@ -73,6 +72,8 @@
"unpinFromTop": "Otkvači sa vrha",
"resetInstallStatusForSelectedAppsQuestion": "Resetujte status instalacije za odabrane aplikacije?",
"installStatusOfXWillBeResetExplanation": "Status instalacije bilo koje odabrane aplikacije će se resetovati.\n\nTo može pomoći kada je verzija aplikacije prikazana u Obtainiumu netačna zbog neuspjelih ažuriranja ili drugih problema.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Podijeli odabrane URL-ove aplikacija",
"resetInstallStatus": "Resetujte status instalacije",
"more": "Više",
@ -211,6 +212,7 @@
"changes": "Promjene",
"releaseDate": "Datum izdavanja",
"importFromURLsInFile": "Uvoz iz URL-ova u datoteci (kao što je OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Otkrivanje verzije",
"standardVersionDetection": "Detekcija standardne verzije",
"groupByCategory": "Grupiši po kategoriji",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Pokrok: {}%",
"pleaseWait": "Počkejte prosím",
"updateAvailable": "Aktualizace je k dispozici",
"estimateInBracketsShort": "(approx.)",
"notInstalled": "Není nainstalováno",
"estimateInBrackets": "(přibližně)",
"pseudoVersion": "pseudo-version",
"selectAll": "Vybrat vše",
"deselectX": "{} deselected",
"xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
@ -73,6 +72,8 @@
"unpinFromTop": "Odepnout shora",
"resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
"installStatusOfXWillBeResetExplanation": "Stav instalace vybraných aplikací bude resetován. To může být užitečné, pokud je verze aplikace zobrazená v Obtainium nesprávná z důvodu neúspěšných aktualizací nebo jiných problémů.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
"resetInstallStatus": "Obnovit stav instalace",
"more": "Více",
@ -211,6 +212,7 @@
"changes": "Změny",
"releaseDate": "Datum vydání",
"importFromURLsInFile": "Importovat adresy URL ze souboru (např. OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Detekce verze",
"standardVersionDetection": "Standardní detekce verze",
"groupByCategory": "Seskupit podle kategorie",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Správce",
"shizukuBinderNotFound": "Shizuku neběží",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Odstranit Apku?",
"other": "Odstranit Apky?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Fortschritt: {}%",
"pleaseWait": "Bitte warten",
"updateAvailable": "Aktualisierung verfügbar",
"estimateInBracketsShort": "(ca.)",
"notInstalled": "Nicht installiert",
"estimateInBrackets": "(Ungefähr)",
"pseudoVersion": "pseudo-version",
"selectAll": "Alle auswählen",
"deselectX": "{} abgewählt",
"xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
@ -73,6 +72,8 @@
"unpinFromTop": "„Oben anheften“ aufheben",
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
"installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.",
"customLinkMessage": "Diese Links funktionieren auf Geräten, wo Obtainium installiert ist",
"shareAppConfigLinks": "Teile die Appkonfiguration als HTML-Link",
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
"resetInstallStatus": "Installationsstatus zurücksetzen",
"more": "Mehr",
@ -211,6 +212,7 @@
"changes": "Änderungen",
"releaseDate": "Veröffentlichungsdatum",
"importFromURLsInFile": "Importieren von URLs aus Datei (z. B. OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Versionserkennung",
"standardVersionDetection": "Standardversionserkennung",
"groupByCategory": "Nach Kategorie gruppieren",
@ -286,7 +288,20 @@
"normal": "Normal",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku läuft nicht",
"shizukuBinderNotFound": "Kompatibler Shizukudienst wurde nicht gefunden",
"useSystemFont": "Verwende die Systemschriftart",
"systemFontError": "Fehler beim Laden der Systemschriftart: {}",
"useVersionCodeAsOSVersion": "Verwende die Appversion als erkannte Version vom Betriebssystem",
"requestHeader": "Request Header",
"useLatestAssetDateAsReleaseDate": "Den letzten Asset-Upload als Veröffentlichungsdatum verwenden",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Progress: {}%",
"pleaseWait": "Please Wait",
"updateAvailable": "Update Available",
"estimateInBracketsShort": "(Est.)",
"notInstalled": "Not Installed",
"estimateInBrackets": "(Estimate)",
"pseudoVersion": "pseudo-version",
"selectAll": "Select All",
"deselectX": "Deselect {}",
"xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.",
@ -73,6 +72,8 @@
"unpinFromTop": "Unpin from top",
"resetInstallStatusForSelectedAppsQuestion": "Reset Install Status for Selected Apps?",
"installStatusOfXWillBeResetExplanation": "The install status of any selected Apps will be reset.\n\nThis can help when the App version shown in Obtainium is incorrect due to failed updates or other issues.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Share Selected App URLs",
"resetInstallStatus": "Reset Install Status",
"more": "More",
@ -164,8 +165,8 @@
"unknown": "Unknown",
"none": "None",
"never": "Never",
"latestVersionX": "Latest Version: {}",
"installedVersionX": "Installed Version: {}",
"latestVersionX": "Latest: {}",
"installedVersionX": "Installed: {}",
"lastUpdateCheckX": "Last Update Check: {}",
"remove": "Remove",
"yesMarkUpdated": "Yes, Mark as Updated",
@ -206,11 +207,12 @@
"removeFromObtainium": "Remove from Obtainium",
"uninstallFromDevice": "Uninstall from Device",
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
"releaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersion": "Use release date as version string",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"importFromURLsInFile": "Import from URLs in File (like OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Version Detection",
"standardVersionDetection": "Standard version detection",
"groupByCategory": "Group by Category",
@ -252,8 +254,8 @@
"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 for Version Extraction Regex",
"versionExtractionRegEx": "Version String Extraction RegEx",
"matchGroupToUse": "Match Group to Use for Version String Extraction Regex",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
@ -267,7 +269,7 @@
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
"versionExtractWholePage": "Apply version string extraction Regex to entire page",
"installing": "Installing",
"skipUpdateNotifications": "Skip update notifications",
"updatesAvailableNotifChannel": "Updates Available",
@ -289,6 +291,17 @@
"shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
"useSystemFont": "Use the system font",
"systemFontError": "Error loading the system font: {}",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Progreso: {}%",
"pleaseWait": "Por favor, espere",
"updateAvailable": "Actualización Disponible",
"estimateInBracketsShort": "(Aprox.)",
"notInstalled": "No Instalado",
"estimateInBrackets": "(Aproximado)",
"pseudoVersion": "pseudo-version",
"selectAll": "Seleccionar Todo",
"deselectX": "Deseleccionar {}",
"xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
@ -73,6 +72,8 @@
"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 útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Compartir URLs de las aplicaciones seleccionadas",
"resetInstallStatus": "Restaurar Estado de Instalación",
"more": "Más",
@ -211,6 +212,7 @@
"changes": "Cambios",
"releaseDate": "Fecha de Publicación",
"importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Detección de Versiones",
"standardVersionDetection": "Por versión",
"groupByCategory": "Agrupar por categoría",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku no está operativo",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"

View File

@ -51,9 +51,8 @@
"percentProgress": "پیش رفتن: {}%",
"pleaseWait": "لطفا صبر کنید",
"updateAvailable": "بروزرسانی در دسترس",
"estimateInBracketsShort": "(تخمین)",
"notInstalled": "نصب نشده",
"estimateInBrackets": "(تخمین زدن)",
"pseudoVersion": "pseudo-version",
"selectAll": "انتخاب همه",
"deselectX": "لغو انتخاب {}",
"xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف می‌شود اما روی دستگاه نصب می‌ماند.",
@ -73,6 +72,8 @@
"unpinFromTop": "برداشتن پین از بالا",
"resetInstallStatusForSelectedAppsQuestion": "وضعیت نصب برنامه‌های انتخابی بازنشانی شود؟",
"installStatusOfXWillBeResetExplanation": "وضعیت نصب برنامه‌های انتخاب‌شده بازنشانی می‌شود.\n\nاگر نسخه برنامه نشان‌داده‌شده در Obtainium به دلیل به‌روزرسانی‌های ناموفق یا مشکلات دیگر نادرست باشد، می‌تواند کمک کند.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "اشتراک گذاری آدرس اینترنتی برنامه های انتخاب شده",
"resetInstallStatus": "بازنشانی وضعیت نصب",
"more": "بیشتر",
@ -85,22 +86,22 @@
"author": "سازنده",
"upToDateApps": "برنامه های به روز",
"nonInstalledApps": "برنامه های نصب نشده",
"importExport": "وادر کردن/صادر کردن",
"importExport": "درون ریزی/برون ریزی",
"settings": "تنظیمات",
"exportedTo": "صادر کردن به{}",
"exportedTo": "برون ریزی به{}",
"obtainiumExport": "صادرکردن Obtainium",
"invalidInput": "ورودی نامعتبر",
"importedX": "وارد شده {}",
"obtainiumImport": "واردکردن Obtainium",
"importFromURLList": "وارد کردن از فهرست آدرس اینترنتی",
"importFromURLList": "درون ریزی از فهرست آدرس اینترنتی",
"searchQuery": "جستجوی سوال",
"appURLList": "فهرست آدرس اینترنتی برنامه",
"line": "خط",
"searchX": "جستجو {}",
"noResults": "نتیجه ای پیدا نشد",
"importX": "وارد کردن {}",
"importedAppsIdDisclaimer": "ممکن است برنامه‌های وارد شده به اشتباه به‌عنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی داده‌های برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روش‌های وارد کردن شخص ثالث تأثیر می‌گذارد.",
"importErrors": "خطاهای وارد کردن",
"importX": "درون ریزی {}",
"importedAppsIdDisclaimer": "ممکن است برنامه‌های وارد شده به اشتباه به‌عنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی داده‌های برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روش‌های درون ریزی شخص ثالث تأثیر می‌گذارد.",
"importErrors": "خطاهای درون ریزی",
"importedXOfYApps": "{} از {} برنامه وارد شد.",
"followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
"selectURL": "آدرس اینترنتی انتخاب شده",
@ -133,7 +134,7 @@
"close": "بستن",
"share": "اشتراک گذاری",
"appNotFound": "برنامه پیدا نشد",
"obtainiumExportHyphenatedLowercase": "صادر کردن-obtainium",
"obtainiumExportHyphenatedLowercase": "برون ریزی-obtainium",
"pickAnAPK": "یک APK انتخاب کنید",
"appHasMoreThanOnePackage": "{} بیش از یک بسته دارد:",
"deviceSupportsXArch": "دستگاه شما از معماری پردازنده {} پشتیبانی میکند",
@ -210,7 +211,8 @@
"releaseDateAsVersionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند، اما تاریخ انتشار در دسترس است.",
"changes": "تغییرات",
"releaseDate": "تاریخ انتشار",
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
"importFromURLsInFile": "درون ریزی از آدرس های اینترنتی موجود در فایل (مانند OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "تشخیص نسخه",
"standardVersionDetection": "تشخیص نسخه استاندارد",
"groupByCategory": "گروه بر اساس دسته",
@ -235,7 +237,7 @@
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
"gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"sortByLastLinkSegment": "فقط بر اساس آخرین بخش پیوند مرتب کنید",
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیش‌فرض '.apk$')",
"appsPossiblyUpdated": "به‌روزرسانی برنامه انجام شد",
@ -245,25 +247,25 @@
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
"backgroundUpdateLimitsExplanation": "موفقیت نصب پس‌زمینه تنها زمانی مشخص می‌شود که Obtainium باز شود.",
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkRegex": "برای بازدید از پیوند «میانگین» فیلتر کنید",
"filterByLinkText": "لینک ها را بر اساس متن پیوند فیلتر کنید",
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
"intermediateLink": "Intermediate link",
"intermediateLink": "پیوند میانی",
"exemptFromBackgroundUpdates": "معاف از به‌روزرسانی‌های پس‌زمینه (در صورت فعال بودن)",
"bgUpdatesOnWiFiOnly": "به‌روزرسانی‌های پس‌زمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
"autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
"versionExtractionRegEx": "نسخه استخراج RegEx",
"matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
"pickExportDir": "فهرست صادرات را انتخاب کنید",
"autoExportOnChanges": "صادرات خودکار تغییرات",
"includeSettings": "Include settings",
"pickExportDir": "فهرست برون ریزی را انتخاب کنید",
"autoExportOnChanges": "برون ریزی خودکار تغییرات",
"includeSettings": "شامل تنظیمات",
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
"dontSortReleasesList": "حفظ سفارش انتشار از API",
"reverseSort": "مرتب سازی معکوس",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"takeFirstLink": "لینک اول را بگیرید",
"skipSort": "از مرتب سازی صرف نظر کنید",
"debugMenu": "منوی اشکال زدایی",
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
"runBgCheckNow": "اکنون به‌روزرسانی پس‌زمینه را بررسی کنید",
@ -281,12 +283,23 @@
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
"selectX": "انتخاب کنید {}",
"parallelDownloads": "Allow parallel downloads",
"installMethod": "Installation method",
"parallelDownloads": "اجازه دانلود موازی",
"installMethod": "روش نصب",
"normal": "Normal",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"shizukuBinderNotFound": "Shizuku در حال اجرا نیست",
"useVersionCodeAsOSVersion": "استفاده کد نسخه برنامه به جای نسخه شناسایی شده توسط سیستم عامل استفاده کنید",
"requestHeader": "درخواست سطر بالایی",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -51,9 +51,8 @@
"percentProgress": "Progrès: {}%",
"pleaseWait": "Veuillez patienter",
"updateAvailable": "Mise à jour disponible",
"estimateInBracketsShort": "(Est.)",
"notInstalled": "Pas installé",
"estimateInBrackets": "(Estimation)",
"pseudoVersion": "pseudo-version",
"selectAll": "Tout sélectionner",
"deselectX": "Déselectionner {}",
"xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.",
@ -73,6 +72,8 @@
"unpinFromTop": "Détacher du haut",
"resetInstallStatusForSelectedAppsQuestion": "Réinitialiser l'état d'installation des applications sélectionnées ?",
"installStatusOfXWillBeResetExplanation": "L'état d'installation de toutes les applications sélectionnées sera réinitialisé.\n\nCela peut aider lorsque la version de l'application affichée dans Obtainium est incorrecte en raison d'échecs de mises à jour ou d'autres problèmes.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Partager les URL d'application sélectionnées",
"resetInstallStatus": "Réinitialiser le statut d'installation",
"more": "Plus",
@ -211,6 +212,7 @@
"changes": "Changements",
"releaseDate": "Date de sortie",
"importFromURLsInFile": "Importer à partir d'URL dans un fichier (comme OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Détection des versions",
"standardVersionDetection": "Détection de version standard",
"groupByCategory": "Group by Category",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Folyamat: {}%",
"pleaseWait": "Kis türelmet",
"updateAvailable": "Frissítés érhető el",
"estimateInBracketsShort": "(Becsült)",
"notInstalled": "Nem telepített",
"estimateInBrackets": "(Becslés)",
"pseudoVersion": "pseudo-version",
"selectAll": "Mindet kiválaszt",
"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.",
@ -73,6 +72,8 @@
"unpinFromTop": "Eltávolít felülről",
"resetInstallStatusForSelectedAppsQuestion": "Visszaállítja a kiválasztott appok telepítési állapotát?",
"installStatusOfXWillBeResetExplanation": "A kiválasztott appok telepítési állapota visszaáll.\n\nEz akkor segíthet, ha az Obtainiumban megjelenített app verzió hibás, frissítések vagy egyéb problémák miatt.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit",
"resetInstallStatus": "Telepítési állapot visszaállítása",
"more": "További",
@ -211,6 +212,7 @@
"changes": "Változtatások",
"releaseDate": "Kiadás dátuma",
"importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Verzió érzékelés",
"standardVersionDetection": "Alapért. verzió érzékelés",
"groupByCategory": "Csoportosítás Kategória alapján",
@ -244,10 +246,10 @@
"backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
"backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Közvetítő link nem található",
"intermediateLink": "Intermediate link",
"intermediateLinkRegex": "Szűrés egy 'köztes' látogatási linkre",
"filterByLinkText": "A hivatkozások szűrése linkszöveg alapján",
"intermediateLinkNotFound": "Köztes link nem található",
"intermediateLink": "Köztes link",
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
@ -261,8 +263,8 @@
"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",
"takeFirstLink": "Vegye az első linket",
"skipSort": "A válogatás kihagyása",
"debugMenu": "Hibakereső menü",
"bgTaskStarted": "A háttérfeladat elindult ellenőrizze a naplókat.",
"enableBackgroundUpdates": "Frissítések a háttérben",
@ -282,11 +284,22 @@
"supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
"selectX": "Kiválaszt {}",
"parallelDownloads": "Párhuzamos letöltéseket enged",
"installMethod": "Installation method",
"normal": "Normal",
"installMethod": "Telepítési mód",
"normal": "Normál",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"shizukuBinderNotFound": "A Shizuku nem fut",
"useVersionCodeAsOSVersion": "Az app versionCode használata a rendszer által észlelt verzióként",
"requestHeader": "Kérelem fejléc",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Avanzamento: {}%",
"pleaseWait": "In attesa",
"updateAvailable": "Aggiornamento disponibile",
"estimateInBracketsShort": "(stim.)",
"notInstalled": "Non installato",
"estimateInBrackets": "(stimato)",
"pseudoVersion": "pseudo-version",
"selectAll": "Seleziona tutto",
"deselectX": "Deseleziona {}",
"xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
@ -73,6 +72,8 @@
"unpinFromTop": "Rimuovi dall'alto",
"resetInstallStatusForSelectedAppsQuestion": "Ripristinare lo stato d'installazione delle app selezionate?",
"installStatusOfXWillBeResetExplanation": "Lo stato d'installazione di ogni app selezionata sarà ripristinato.\n\nCiò può essere d'aiuto nel caso in cui la versione mostrata dell'app in Obtainium non sia corretta a causa di un aggiornamento fallito o di altri problemi.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Condividi gli URL delle app selezionate",
"resetInstallStatus": "Ripristina lo stato d'installazione",
"more": "Altro",
@ -211,6 +212,7 @@
"changes": "Novità",
"releaseDate": "Data di rilascio",
"importFromURLsInFile": "Importa da URL in file (come OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Rilevamento di versione",
"standardVersionDetection": "Rilevamento di versione standard",
"groupByCategory": "Raggruppa per categoria",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku non è in esecuzione",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"

View File

@ -51,9 +51,8 @@
"percentProgress": "ダウンロード中: {}%",
"pleaseWait": "しばらくお待ちください",
"updateAvailable": "アップデートが利用可能",
"estimateInBracketsShort": "(推定)",
"notInstalled": "未インストール",
"estimateInBrackets": "(推定)",
"pseudoVersion": "pseudo-version",
"selectAll": "すべて選択",
"deselectX": "{}件の選択を解除",
"xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
@ -73,6 +72,8 @@
"unpinFromTop": "トップから固定解除",
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
"resetInstallStatus": "インストール状態をリセットする",
"more": "もっと見る",
@ -211,6 +212,7 @@
"changes": "変更点",
"releaseDate": "リリース日",
"importFromURLsInFile": "ファイルOPMLなど内のURLからインポート",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "バージョン検出",
"standardVersionDetection": "標準のバージョン検出",
"groupByCategory": "カテゴリ別にグループ化する",
@ -287,6 +289,19 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizukuが起動していません",
"useSystemFont": "システムフォントを使用する",
"systemFontError": "システムフォントの読み込みエラー: {}",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Vooruitgang: {}%",
"pleaseWait": "Even geduld",
"updateAvailable": "Update beschikbaar",
"estimateInBracketsShort": "(Ong.)",
"notInstalled": "Niet geinstalleerd",
"estimateInBrackets": "(Ongeveer)",
"pseudoVersion": "pseudo-version",
"selectAll": "Selecteer alles",
"deselectX": "Deselecteer {}",
"xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.",
@ -73,6 +72,8 @@
"unpinFromTop": "Losmaken van de bovenkant",
"resetInstallStatusForSelectedAppsQuestion": "Installatiestatus resetten voor geselecteerde apps?",
"installStatusOfXWillBeResetExplanation": "De installatiestatus van alle geselecteerde apps zal worden gereset.\n\nDit kan helpen wanneer de versie van de app die in Obtainium wordt weergegeven onjuist is vanwege mislukte updates of andere problemen.",
"customLinkMessage": "Deze links werken op apparaten waarop Obtainium is geïnstalleerd",
"shareAppConfigLinks": "App-configuratie delen als HTML-link",
"shareSelectedAppURLs": "Deel geselecteerde app URL's",
"resetInstallStatus": "Reset installatiestatus",
"more": "Meer",
@ -211,12 +212,13 @@
"changes": "Veranderingen",
"releaseDate": "Releasedatum",
"importFromURLsInFile": "Importeren vanaf URL's in een bestand (zoals OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Versieherkenning",
"standardVersionDetection": "Standaard versieherkenning",
"groupByCategory": "Groepeer op categorie",
"autoApkFilterByArch": "Poging om APK's te filteren op CPU-architectuur indien mogelijk",
"overrideSource": "Bron overschrijven",
"dontShowAgain": "Don't show this again",
"dontShowAgain": "Laat dit niet meer zien",
"dontShowTrackOnlyWarnings": "Geen waarschuwingen voor 'Track-Only' weergeven",
"dontShowAPKOriginWarnings": "APK-herkomstwaarschuwingen niet weergeven",
"moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
@ -235,7 +237,7 @@
"addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
"githubSourceNote": "Beperkingen van GitHub kunnen worden vermeden door het gebruik van een API-sleutel.",
"gitlabSourceNote": "GitLab APK-extractie werkt mogelijk niet zonder een API-sleutel.",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"sortByLastLinkSegment": "Sorteren op alleen het laatste segment van de link",
"filterReleaseNotesByRegEx": "Filter release-opmerkingen met een reguliere expressie.",
"customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
"appsPossiblyUpdated": "Poging tot app-updates",
@ -245,10 +247,10 @@
"backgroundUpdateReqsExplanation": "Achtergrondupdates zijn mogelijk niet voor alle apps mogelijk.",
"backgroundUpdateLimitsExplanation": "Het succes van een installatie in de achtergrond kan alleen worden bepaald wanneer Obtainium is geopend.",
"verifyLatestTag": "Verifieer de 'Laatste'-tag",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkRegex": "Filter voor een 'Intermediaire' link om te bezoeken",
"filterByLinkText": "Links filteren op linktekst",
"intermediateLinkNotFound": "Tussenliggende link niet gevonden",
"intermediateLink": "Intermediate link",
"intermediateLink": "Intermediaire link",
"exemptFromBackgroundUpdates": "Vrijgesteld van achtergrondupdates (indien ingeschakeld)",
"bgUpdatesOnWiFiOnly": "Achtergrondupdates uitschakelen wanneer niet verbonden met WiFi",
"autoSelectHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
@ -257,13 +259,13 @@
"highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
"pickExportDir": "Kies de exportmap",
"autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
"includeSettings": "Include settings",
"includeSettings": "Instellingen opnemen",
"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",
"takeFirstLink": "Neem de eerste link",
"skipSort": "Sorteren overslaan",
"debugMenu": "Debug menu",
"bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
"runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
@ -279,14 +281,25 @@
"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",
"installMethod": "Installation method",
"normal": "Normal",
"supportFixedAPKURL": "Ondersteuning vaste APK URL's",
"selectX": "Selecteer {}",
"parallelDownloads": "Parallelle downloads toestaan",
"installMethod": "Installatiemethode",
"normal": "Normaal",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"shizukuBinderNotFound": "Shizuku draait niet",
"useVersionCodeAsOSVersion": "Gebruik app versieCode als door OS gedetecteerde versie",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Gebruik laatste upload als releasedatum",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "App verwijderen?",
"other": "Apps verwijderen?"
@ -339,4 +352,4 @@
"one": "{} en nog 1 app zijn mogelijk bijgewerkt.",
"other": "{} en {} meer apps zijn mogelijk bijgwerkt."
}
}
}

View File

@ -51,9 +51,8 @@
"percentProgress": "Postęp: {}%",
"pleaseWait": "Proszę czekać",
"updateAvailable": "Dostępna aktualizacja",
"estimateInBracketsShort": "(Szac.)",
"notInstalled": "Nie zainstalowano",
"estimateInBrackets": "(Szacunkowo)",
"pseudoVersion": "pseudo-version",
"selectAll": "Zaznacz wszystkie",
"deselectX": "Odznacz {}",
"xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
@ -73,6 +72,8 @@
"unpinFromTop": "Odepnij",
"resetInstallStatusForSelectedAppsQuestion": "Zresetować status instalacji dla wybranych aplikacji?",
"installStatusOfXWillBeResetExplanation": "Stan instalacji wybranych aplikacji zostanie zresetowany.\n\nMoże być to pomocne, gdy wersja aplikacji wyświetlana w Obtainium jest nieprawidłowa z powodu nieudanych aktualizacji lub innych problemów.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Udostępnij wybrane adresy URL aplikacji",
"resetInstallStatus": "Zresetuj stan instalacji",
"more": "Więcej",
@ -211,6 +212,7 @@
"changes": "Zmiany",
"releaseDate": "Data wydania",
"importFromURLsInFile": "Importuj z adresów URL w pliku (typu OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Wykrywanie wersji",
"standardVersionDetection": "Standardowe wykrywanie wersji",
"groupByCategory": "Grupuj według kategorii",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?",

View File

@ -54,6 +54,7 @@
"estimateInBracketsShort": "(Aprox.)",
"notInstalled": "Não instalado",
"estimateInBrackets": "(Aproximado)",
"pseudoVersion": "pseudo-version",
"selectAll": "Selecionar todos",
"deselectX": "Deselecionar {}",
"xWillBeRemovedButRemainInstalled": "{} será removido do Obtainium mais permanecerá instalado no dispositivo.",
@ -73,6 +74,8 @@
"unpinFromTop": "Desafixar do topo",
"resetInstallStatusForSelectedAppsQuestion": "Reiniciar status de instalação para aplicativos selecionados?",
"installStatusOfXWillBeResetExplanation": "O status de instalação de qualquer aplicativo selecionado será reiniciado.\n\nIsso pode ajudar quando uma versão de um aplicativo mostrada no Obtainium é incorreta devido a falhas ao atualizar ou outros problemas.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Compartilhar URLs de aplicativos selecionados",
"resetInstallStatus": "Reiniciar status de Iistalação",
"more": "Mais",
@ -211,7 +214,7 @@
"changes": "Mudanças",
"releaseDate": "Data de lançamento",
"importFromURLsInFile": "Importar de URLs em arquivo (como OPML)",
"versionDetection": "Detecção de Versão",
"versionDetection": "Detecção de versão",
"standardVersionDetection": "Detecção de versão padrão",
"groupByCategory": "Agroupar por categoria",
"autoApkFilterByArch": "Tente filtrar APKs por arquitetura de CPU, se possível",
@ -220,79 +223,91 @@
"dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas Monitorar'",
"dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK",
"moveNonInstalledAppsToBottom": "Mover aplicativos não instalados para o fundo da lista de aplicativos",
"gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
"gitlabPATLabel": "Token de Acesso Pessoal do Gitlab\n(Ativa pesquisa e melhora a descoberta de APKs)",
"about": "Sobre",
"requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
"checkOnStart": "Checar por atualizações ao iniciar ",
"checkOnStart": "Verificar se há atualizações ao iniciar",
"tryInferAppIdFromCode": "Tente inferir o ID do aplicativo pelo código-fonte",
"removeOnExternalUninstall": "Remover automaticamente aplicativos desinstalados externamente",
"pickHighestVersionCode": "Auto-selecionar o maior numero de versão do APK",
"pickHighestVersionCode": "Auto-selecionar o maior número de versão do APK",
"checkUpdateOnDetailPage": "Checar por atualizações ao abrir a página de detalhes de um aplicativo",
"disablePageTransitions": "Desativar animações de transição de pagina",
"reversePageTransitions": "Reverter animações de transição de pagina",
"minStarCount": "Contagem Minima de Estrelas",
"disablePageTransitions": "Desativar animações de transição de página",
"reversePageTransitions": "Reverter animações de transição de página",
"minStarCount": "Contagem nima de estrelas",
"addInfoBelow": "Adicionar essa informação abaixo.",
"addInfoInSettings": "Adicionar essa informação nas configurações.",
"githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
"gitlabSourceNote": "A extração de APK do GitLab pode não funcionar sem uma chave de API.",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular",
"customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')",
"gitlabSourceNote": "A extração de endereço de download do APK no GitLab provavelmente não funcione sem que seja fornecido uma chave de API.",
"sortByLastLinkSegment": "Ordenar apenas usando o último segmento do link",
"filterReleaseNotesByRegEx": "Filtrar notas de versão usando Regex",
"customLinkFilterRegex": "Filtro de link personalizado usando expressão regular (Padrão '.apk$')",
"appsPossiblyUpdated": "Tentativas de atualização de aplicativos",
"appsPossiblyUpdatedNotifDescription": "Notifica o usuário de que atualizações de um ou mais aplicativos foram potencialmente aplicadas em segundo-plano",
"xWasPossiblyUpdatedToY": "{} pode ter sido atualizado para {}.",
"enableBackgroundUpdates": "Ativar atualizações em segundo-plano",
"backgroundUpdateReqsExplanation": "Atualizações em segundo-plano podem não ser possíveis para todos os aplicativos.",
"backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo-plano só pode ser determinado quando o Obtainium é aberto.",
"verifyLatestTag": "Verifique a 'ultima' etiqueta",
"verifyLatestTag": "Verifique a 'última' etiqueta",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"filterByLinkText": "Filtrar links pelo texto do link",
"intermediateLinkNotFound": "Link intermediário não encontrado",
"intermediateLink": "Intermediate link",
"intermediateLink": "Link intermediário",
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo-plano (se ativadas)",
"bgUpdatesOnWiFiOnly": "Desative atualizações em segundo-plano quando não estiver em WiFi",
"autoSelectHighestVersionCode": "Auto-selecionar o maior codigo de versão",
"versionExtractionRegEx": "RegEx para Extração de Versão",
"matchGroupToUse": "Grupo de Seleção para Usar",
"highlightTouchTargets": "Destaque areas de toque menos óbvias",
"pickExportDir": "Escolher Diretorio de Exportação",
"bgUpdatesOnWiFiOnly": "Desative as atualizações em segundo-plano quando não estiver conectado no Wi-Fi",
"autoSelectHighestVersionCode": "Auto-selecionar a versão mais recente",
"versionExtractionRegEx": "Regex de extração de versão",
"matchGroupToUse": "Grupo correspondente a ser usado no Regex de extração de versão",
"highlightTouchTargets": "Realçar áreas sensíveis ao toque que são menos óbvias",
"pickExportDir": "Escolher diretório para a exportação",
"autoExportOnChanges": "Auto-exportar em mudanças",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
"includeSettings": "Incluir configurações",
"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",
"takeFirstLink": "Obter primeiro link",
"skipSort": "Ignorar ordenação",
"debugMenu": "Menu debug",
"bgTaskStarted": "Tarefa em segundo-plano iniciada - verifique os logs.",
"runBgCheckNow": "Execute a verificação de atualização em segundo-plano agora",
"versionExtractWholePage": "Aplicar Regex de Extração de Versão à Página Inteira",
"runBgCheckNow": "Execute agora em segundo-plano a verificação de atualizações",
"versionExtractWholePage": "Aplicar regex de extração de versão à página inteira",
"installing": "Instalando",
"skipUpdateNotifications": "Pular notificações de update",
"updatesAvailableNotifChannel": "Atualizações Disponíveis",
"appsUpdatedNotifChannel": "Aplicativos Atualizados",
"updatesAvailableNotifChannel": "Atualizações disponíveis",
"appsUpdatedNotifChannel": "Aplicativos atualizados",
"appsPossiblyUpdatedNotifChannel": "Tentativas de atualização de aplicativos",
"errorCheckingUpdatesNotifChannel": "Erro ao Procurar por Atualizações",
"appsRemovedNotifChannel": "Aplicativos Removidos",
"appsRemovedNotifChannel": "Aplicativos removidos",
"downloadingXNotifChannel": "Baixando {}",
"completeAppInstallationNotifChannel": "Instalação completa do aplicativo",
"checkingForUpdatesNotifChannel": "Checando por Atualizações",
"onlyCheckInstalledOrTrackOnlyApps": "Apenas checar apps instalados e 'Apenas Seguir' por updates",
"supportFixedAPKURL": "Suporte APK com URLs fixas",
"onlyCheckInstalledOrTrackOnlyApps": "Apenas checar aplicativos instalados e 'Apenas Seguir' por updates",
"supportFixedAPKURL": "Suporte a APK com URLs fixas",
"selectX": "Selecionar {}",
"parallelDownloads": "Permitir downloads paralelos",
"installMethod": "Método de instalação",
"normal": "Normal",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku não está rodando",
"shizukuBinderNotFound": "O Shizuku não está rodando",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"useSystemFont": "Usar fonte padrão do sistema",
"systemFontError": "Erro ao carregar a fonte do sistema: {}",
"requestHeader": "Requisitar cabeçalho",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Remover aplicativo?",
"other": "Remover aplicativos?"
},
"tooManyRequestsTryAgainInMinutes": {
"one": "Muitas solicitações (taxa limitada) - tente novamente em {} minuto",
"one": "Muitas solicitações (taxa de solicitações limitada) - tente novamente em {} minuto",
"other": "Muitas solicitações (taxa limitada) - tente novamente em {} minutos"
},
"bgUpdateGotErrorRetryInMinutes": {
@ -324,19 +339,19 @@
"other": "{} Dias"
},
"clearedNLogsBeforeXAfterY": {
"one": "Limpo {n} log (antes = {antes}, depois = {depois})",
"other": "Limpados {n} logs (antes = {antes}, depois = {depois})"
"one": "Foi limpo {n} log (antes = {antes}, depois = {depois})",
"other": "Foram limpos {n} logs (antes = {antes}, depois = {depois})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} e 1 outro app tem atualizações.",
"other": "{} e {} outros apps tem atualizações."
"one": "{} e 1 outro aplicativo possui atualizações.",
"other": "{} e {} outros aplicativo possuem atualizações."
},
"xAndNMoreUpdatesInstalled": {
"one": "{} e 1 outro app foi atualizado.",
"other": "{} e {} outros apps foram atualizados."
"one": "{} e um outro aplicativo foi atualizado.",
"other": "{} e {} outros aplicativos foram atualizados."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} e 1 outro app pode ter sido atualizado.",
"other": "{} e {} outros apps podem ter sido atualizados."
"one": "{} e 1 outro aplicativo pode ter sido atualizado.",
"other": "{} e {} outros aplicativos podem ter sido atualizados."
}
}

View File

@ -51,9 +51,8 @@
"percentProgress": "Прогресс: {}%",
"pleaseWait": "Пожалуйста, подождите",
"updateAvailable": "Доступно обновление",
"estimateInBracketsShort": "(Оценка)",
"notInstalled": "Не установлено",
"estimateInBrackets": "(Оценка)",
"pseudoVersion": "pseudo-version",
"selectAll": "Выбрать всё",
"deselectX": "Отменить выбор {}",
"xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве",
@ -73,6 +72,8 @@
"unpinFromTop": "Открепить",
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, некорректная — из-за неудачных обновлений или других проблем",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Поделиться выбранными URL-адресами приложений",
"resetInstallStatus": "Сбросить статус установки",
"more": "Ещё",
@ -211,6 +212,7 @@
"changes": "Изменения",
"releaseDate": "Дата выпуска",
"importFromURLsInFile": "Импорт из файла URL-адресов (например: OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Определение версии",
"standardVersionDetection": "Стандартное",
"groupByCategory": "Группировать по категориям",
@ -289,6 +291,17 @@
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
"useSystemFont": "Использовать системный шрифт",
"systemFontError": "Ошибка загрузки системного шрифта: {}",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"

View File

@ -51,9 +51,8 @@
"percentProgress": "Progress: {}%",
"pleaseWait": "Vänta",
"updateAvailable": "Uppdatering Tillgänglig",
"estimateInBracketsShort": "(Est.)",
"notInstalled": "Inte Installerad",
"estimateInBrackets": "(Uppskattning)",
"pseudoVersion": "pseudo-version",
"selectAll": "Välj Alla",
"deselectX": "Avmarkera {}",
"xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.",
@ -73,6 +72,8 @@
"unpinFromTop": "Avnåla",
"resetInstallStatusForSelectedAppsQuestion": "Återställ Installationsstatus för valda Appar?",
"installStatusOfXWillBeResetExplanation": "Installationsstatusen för de markerade apparna kommer återställas.\n\n Detta kan hjälpa när appversionen visad i Obtanium är fel på grund av misslyckade uppdateringar eller andra orsaker.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Dela Valda Appars URL:er",
"resetInstallStatus": "Återställ Installationstatus",
"more": "Mer",
@ -211,6 +212,7 @@
"changes": "Ändringar",
"releaseDate": "Releasedatum",
"importFromURLsInFile": "Importera från URL:er i fil (som OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Versionsdetektering",
"standardVersionDetection": "Standardversionsdetektering",
"groupByCategory": "Gruppera via Kategori",
@ -273,6 +275,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Ta Bort App?",
"other": "Ta Bort Appar?"

View File

@ -51,9 +51,8 @@
"percentProgress": "İlerleme: {}%",
"pleaseWait": "Lütfen Bekleyin",
"updateAvailable": "Güncelleme Var",
"estimateInBracketsShort": "(Est.)",
"notInstalled": "Yüklenmedi",
"estimateInBrackets": "(Tahmini)",
"pseudoVersion": "pseudo-version",
"selectAll": "Hepsini Seç",
"deselectX": "{}'yi Seçimden Kaldır",
"xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.",
@ -73,6 +72,8 @@
"unpinFromTop": "Üstten Kaldır",
"resetInstallStatusForSelectedAppsQuestion": "Seçilen Uygulamaların Yükleme Durumunu Sıfırlamak İstiyor musunuz?",
"installStatusOfXWillBeResetExplanation": "Seçilen Uygulamaların yükleme durumu sıfırlanacak.\n\nBu, Obtainium'da gösterilen uygulama sürümünün başarısız güncellemeler veya diğer sorunlar nedeniyle yanlış olması durumunda yardımcı olabilir.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Seçilen Uygulama URL'larını Paylaş",
"resetInstallStatus": "Yükleme Durumunu Sıfırla",
"more": "Daha Fazla",
@ -211,6 +212,7 @@
"changes": "Değişiklikler",
"releaseDate": "Yayın Tarihi",
"importFromURLsInFile": "Dosyadaki URL'lerden İçe Aktar",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Sürüm Tespiti",
"standardVersionDetection": "Standart sürüm tespiti",
"groupByCategory": "Kategoriye Göre Grupla",
@ -287,6 +289,17 @@
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "Uygulamayı Kaldır?",
"other": "Uygulamaları Kaldır?"

View File

@ -11,7 +11,7 @@
"unexpectedError": "Lỗi không mong đợi",
"ok": "OK",
"and": "và",
"githubPATLabel": "Mã thông báo truy cập cá nhân GitHub (Tăng tốc độ giới hạn)",
"githubPATLabel": "GitHub Token (Tăng tốc độ, giới hạn)",
"includePrereleases": "Bao gồm các bản phát hành trước",
"fallbackToOlderReleases": "Dự phòng về bản phát hành cũ hơn",
"filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy",
@ -34,7 +34,7 @@
"cancelled": "Đã hủy",
"appAlreadyAdded": "Ứng dụng được thêm rồi",
"alreadyUpToDateQuestion": "Ứng dụng đã được cập nhật?",
"addApp": "Thêm ứng dụng",
"addApp": "Thêm",
"appSourceURL": "URL nguồn ứng dụng",
"error": "Lỗi",
"add": "Thêm",
@ -48,12 +48,11 @@
"noApps": "Không có ứng dụng",
"noAppsForFilter": "Không có ứng dụng cho bộ lọc",
"byX": "Bởi {}",
"percentProgress": "Tiến triển: {}%",
"percentProgress": "Đang tải {}%",
"pleaseWait": "Vui lòng chờ",
"updateAvailable": "Có sẵn bản cập nhật",
"estimateInBracketsShort": "(Ước lượng.)",
"notInstalled": "Chưa cài đặt",
"estimateInBrackets": "(Ước lượng)",
"pseudoVersion": "pseudo-version",
"selectAll": "Chọn tất cả",
"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ị.",
@ -73,6 +72,8 @@
"unpinFromTop": "Bỏ ghim khỏi đầu trang",
"resetInstallStatusForSelectedAppsQuestion": "Đặt lại trạng thái cài đặt cho ứng dụng đã chọn?",
"installStatusOfXWillBeResetExplanation": "Trạng thái cài đặt của mọi Ứng dụng đã chọn sẽ được đặt lại.\n\nĐiều này có thể hữu ích khi phiên bản Ứng dụng hiển thị trong Obtainium không chính xác do cập nhật không thành công hoặc các sự cố khác.",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "Chia sẻ URL ứng dụng đã chọn",
"resetInstallStatus": "Đặt lại trạng thái cài đặt",
"more": "Nhiều hơn",
@ -88,10 +89,10 @@
"importExport": "Nhập/Xuất",
"settings": "Cài đặt",
"exportedTo": "Đã xuất sang {}",
"obtainiumExport": "Xuất Obtainium",
"obtainiumExport": "Xuất",
"invalidInput": "Đầu vào không hợp lệ",
"importedX": "Đã nhập {}",
"obtainiumImport": "Nhập Obtainium",
"obtainiumImport": "Nhập",
"importFromURLList": "Nhập từ danh sách URL",
"searchQuery": "Truy vấn tìm kiếm",
"appURLList": "Danh sách URL ứng dụng",
@ -120,13 +121,13 @@
"appSortOrder": "Thứ tự sắp xếp",
"ascending": "Tăng dần",
"descending": "Giảm dần",
"bgUpdateCheckInterval": "Khoảng thời gian kiểm tra cập nhật nền",
"neverManualOnly": "Không bao giờ - Chỉ thủ công",
"bgUpdateCheckInterval": "Thời gian tự động kiểm tra cập nhật",
"neverManualOnly": "Không bao giờ",
"appearance": "Hiển thị",
"showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem Ứng dụng",
"pinUpdates": "Ghim nội dung cập nhật lên đầu chế độ xem Ứng dụng",
"showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem chi tiết Ứng dụng",
"pinUpdates": "Chuyển ứng dng có phiên bản mới lên đầu danh sách",
"updates": "Cập nhật",
"sourceSpecific": "Nguồn cụ thể",
"sourceSpecific": "Cài đặt Nguồn",
"appSource": "Nguồn ứng dụng",
"noLogs": "Không có nhật ký",
"appLogs": "Nhật ký ứng dụng",
@ -211,6 +212,7 @@
"changes": "Thay đổi",
"releaseDate": "Ngày phát hành",
"importFromURLsInFile": "Nhập từ URL trong Tệp (như OPML)",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "Phát hiện phiên bản",
"standardVersionDetection": "Phát hiện phiên bản tiêu chuẩn",
"groupByCategory": "Nhóm theo thể loại",
@ -219,8 +221,8 @@
"dontShowAgain": "Đừng hiển thị thông tin này nữa",
"dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ-Theo dõi'",
"dontShowAPKOriginWarnings": "Không hiển thị cảnh báo nguồn gốc APK",
"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)",
"moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách",
"gitlabPATLabel": "GitLab Token\n(Cho phép tìm kiếm và lọc 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)",
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
@ -241,9 +243,9 @@
"appsPossiblyUpdated": "Đã cố gắng cập nhật ứng dụng",
"appsPossiblyUpdatedNotifDescription": "Thông báo cho người dùng rằng các bản cập nhật cho một hoặc nhiều Ứng dụng có khả năng được áp dụng trong nền",
"xWasPossiblyUpdatedToY": "{} có thể đã được cập nhật thành {}.",
"enableBackgroundUpdates": "Bật cập nhật nền",
"backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật nền cho tất cả ứng dụng.",
"backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt nền chỉ có thể được xác định khi mở Obtainium.",
"enableBackgroundUpdates": "Tự động cập nhật trong nền",
"backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật trong nền cho tất cả ứng dụng.",
"backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt trong nền chỉ có thể được xác định khi mở Obtainium.",
"verifyLatestTag": "Xác minh thẻ 'mới nhất'",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
@ -256,8 +258,8 @@
"matchGroupToUse": "Nhóm đối sánh để sử dụng cho Regex trích xuất phiên bản",
"highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
"pickExportDir": "Chọn thư mục xuất",
"autoExportOnChanges": "Tự động xuất khi thay đổi",
"includeSettings": "Include settings",
"autoExportOnChanges": "Tự động xuất",
"includeSettings": "Bao gồm cài đặt ứng dụng",
"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",
@ -278,15 +280,24 @@
"downloadingXNotifChannel": "Đang tải xuống {}",
"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",
"onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra cập nhật các ứng dụng đã cài đặt và Chỉ-Theo dõi",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"installMethod": "Installation method",
"normal": "Normal",
"parallelDownloads": "Cho phép tải đa luồng",
"installMethod": "Phương thức cài đặt",
"normal": "Mặc định",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku chưa khởi động",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion":{
"one": "Gỡ ứng dụng?",
"other": "Gỡ ứng dụng?"

View File

@ -51,9 +51,8 @@
"percentProgress": "进度:{}%",
"pleaseWait": "请稍候",
"updateAvailable": "更新可用",
"estimateInBracketsShort": "(推测)",
"notInstalled": "未安装",
"estimateInBrackets": "(推测)",
"pseudoVersion": "pseudo-version",
"selectAll": "全选",
"deselectX": "取消选择 {}",
"xWillBeRemovedButRemainInstalled": "“{}”将从 Obtainium 中删除,但仍安装在您的设备中。",
@ -73,6 +72,8 @@
"unpinFromTop": "取消置顶",
"resetInstallStatusForSelectedAppsQuestion": "是否重置选中应用的安装状态?",
"installStatusOfXWillBeResetExplanation": "选中应用的安装状态将会被重置。\n\n当更新安装失败或其他问题导致 Obtainium 中的应用版本显示错误时,可以尝试通过此方法解决。",
"customLinkMessage": "These links work on devices with Obtainium installed",
"shareAppConfigLinks": "Share app configuration as HTML link",
"shareSelectedAppURLs": "分享选中应用的 URL",
"resetInstallStatus": "重置安装状态",
"more": "更多",
@ -211,6 +212,7 @@
"changes": "更新日志",
"releaseDate": "发行日期",
"importFromURLsInFile": "从文件中的 URL 导入(如 OPML",
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
"versionDetection": "版本检测",
"standardVersionDetection": "常规版本检测",
"groupByCategory": "按类别分组显示",
@ -289,6 +291,17 @@
"shizukuBinderNotFound": "未发现兼容的 Shizuku 服务",
"useSystemFont": "使用系统字体",
"systemFontError": "加载系统字体出错:{}",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
"partialAPKHash": "Partial APK Hash",
"APKLinkHash": "APK Link Hash",
"directAPKLink": "Direct APK Link",
"pseudoVersionInUse": "A Pseudo-Version is in Use",
"installed": "Installed",
"latest": "Latest",
"invertRegEx": "Invert regular expression",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -5,17 +5,20 @@ import 'package:obtainium/providers/source_provider.dart';
class APKCombo extends AppSource {
APKCombo() {
host = 'apkcombo.com';
hosts = ['apkcombo.com'];
showReleaseDateAsVersionToggle = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+');
var match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
caseSensitive: false);
var match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -26,18 +29,19 @@ class APKCombo extends AppSource {
@override
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
return {
"User-Agent": "curl/8.0.1",
"Accept": "*/*",
"Connection": "keep-alive",
"Host": "$host"
"Host": hosts[0]
};
}
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
var res = await sourceRequest('$standardUrl/download/apk');
Future<List<MapEntry<String, String>>> getApkUrls(
String standardUrl, Map<String, dynamic> additionalSettings) async {
var res = await sourceRequest('$standardUrl/download/apk', {});
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
@ -70,9 +74,9 @@ class APKCombo extends AppSource {
}
@override
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
var freshURLs = await getApkUrls(standardUrl);
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
Map<String, dynamic> additionalSettings) async {
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
var path2Match = Uri.parse(apkUrl).path;
for (var url in freshURLs) {
if (Uri.parse(url.value).path == path2Match) {
@ -88,7 +92,7 @@ class APKCombo extends AppSource {
Map<String, dynamic> additionalSettings,
) async {
String appId = (await tryInferringAppId(standardUrl))!;
var preres = await sourceRequest(standardUrl);
var preres = await sourceRequest(standardUrl, additionalSettings);
if (preres.statusCode != 200) {
throw getObtainiumHttpError(preres);
}
@ -112,7 +116,9 @@ class APKCombo extends AppSource {
}
}
return APKDetails(
version, await getApkUrls(standardUrl), AppNames(author, appName),
version,
await getApkUrls(standardUrl, additionalSettings),
AppNames(author, appName),
releaseDate: releaseDate);
}
}

View File

@ -9,8 +9,9 @@ import 'package:obtainium/providers/source_provider.dart';
class APKMirror extends AppSource {
APKMirror() {
host = 'apkmirror.com';
hosts = ['apkmirror.com'];
enforceTrackOnly = true;
showReleaseDateAsVersionToggle = true;
additionalSourceAppSpecificSettingFormItems = [
[
@ -32,13 +33,14 @@ class APKMirror extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx =
RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -58,7 +60,7 @@ class APKMirror extends AppSource {
true
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await sourceRequest('$standardUrl/feed');
Response res = await sourceRequest('$standardUrl/feed', additionalSettings);
if (res.statusCode == 200) {
var items = parse(res.body).querySelectorAll('item');
dynamic targetRelease;
@ -84,7 +86,7 @@ class APKMirror extends AppSource {
dateString != null ? HttpDate.parse('$dateString GMT') : null;
String? version = titleString
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
RegExp(' by ').firstMatch(titleString)?.start ?? 0)
RegExp(' by ').allMatches(titleString).last.start)
.trim();
if (version == null || version.isEmpty) {
version = titleString;

View File

@ -20,26 +20,29 @@ parseDateTimeMMMddCommayyyy(String? dateString) {
class APKPure extends AppSource {
APKPure() {
host = 'apkpure.com';
hosts = ['apkpure.net', 'apkpure.com'];
allowSubDomains = true;
naiveStandardVersionDetection = true;
showReleaseDateAsVersionToggle = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExB =
RegExp('^https?://m.$host/+[^/]+/+[^/]+(/+[^/]+)?');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
RegExp standardUrlRegExB = RegExp(
'^https?://m.${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?',
caseSensitive: false);
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
if (match != null) {
url = 'https://$host${Uri.parse(url).path}';
url = 'https://${getSourceRegex(hosts)}${Uri.parse(url).path}';
}
RegExp standardUrlRegExA =
RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+(/+[^/]+)?');
match = standardUrlRegExA.firstMatch(url.toLowerCase());
RegExp standardUrlRegExA = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?',
caseSensitive: false);
match = standardUrlRegExA.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -55,8 +58,8 @@ class APKPure extends AppSource {
) async {
String appId = (await tryInferringAppId(standardUrl))!;
String host = Uri.parse(standardUrl).host;
var res = await sourceRequest('$standardUrl/download');
var resChangelog = await sourceRequest(standardUrl);
var res = await sourceRequest('$standardUrl/download', additionalSettings);
var resChangelog = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode == 200 && resChangelog.statusCode == 200) {
var html = parse(res.body);
var htmlChangelog = parse(resChangelog.body);
@ -69,7 +72,8 @@ class APKPure extends AppSource {
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
List<MapEntry<String, String>> apkUrls = [
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
MapEntry('$appId.apk',
'https://d.${hosts.contains(host) ? 'cdnpure.com' : host}/b/$type/$appId?version=latest')
];
String author = html
.querySelector('span.info-sdk')

View File

@ -6,30 +6,35 @@ import 'package:obtainium/providers/source_provider.dart';
class Aptoide extends AppSource {
Aptoide() {
host = 'aptoide.com';
hosts = ['aptoide.com'];
name = 'Aptoide';
allowSubDomains = true;
naiveStandardVersionDetection = true;
showReleaseDateAsVersionToggle = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async {
return (await getAppDetailsJSON(standardUrl))['package'];
return (await getAppDetailsJSON(
standardUrl, additionalSettings))['package'];
}
Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async {
var res = await sourceRequest(standardUrl);
Future<Map<String, dynamic>> getAppDetailsJSON(
String standardUrl, Map<String, dynamic> additionalSettings) async {
var res = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
@ -40,8 +45,8 @@ class Aptoide extends AppSource {
} else {
throw NoReleasesError();
}
var res2 =
await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id');
var res2 = await sourceRequest(
'https://ws2.aptoide.com/api/7/getApp/app_id/$id', additionalSettings);
if (res2.statusCode != 200) {
throw getObtainiumHttpError(res);
}
@ -53,7 +58,7 @@ class Aptoide extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var appDetails = await getAppDetailsJSON(standardUrl);
var appDetails = await getAppDetailsJSON(standardUrl, additionalSettings);
String appName = appDetails['name'] ?? tr('app');
String author = appDetails['developer']?['name'] ?? name;
String? dateStr = appDetails['updated'];

View File

@ -5,7 +5,7 @@ import 'package:obtainium/providers/source_provider.dart';
class Codeberg extends AppSource {
GitHub gh = GitHub();
Codeberg() {
host = 'codeberg.org';
hosts = ['codeberg.org'];
additionalSourceAppSpecificSettingFormItems =
gh.additionalSourceAppSpecificSettingFormItems;
@ -16,12 +16,14 @@ class Codeberg extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -35,7 +37,7 @@ class Codeberg extends AppSource {
) async {
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
(bool useTagUrl) async {
return 'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
}, null);
}
@ -50,7 +52,7 @@ class Codeberg extends AppSource {
{Map<String, dynamic> querySettings = const {}}) async {
return gh.searchCommon(
query,
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
'data',
querySettings: querySettings);
}

View File

@ -0,0 +1,44 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/providers/source_provider.dart';
class DirectAPKLink extends AppSource {
HTML html = HTML();
DirectAPKLink() {
neverAutoSelect = true;
name = tr('directAPKLink');
additionalSourceAppSpecificSettingFormItems = html
.additionalSourceAppSpecificSettingFormItems
.where((element) => element
.where((element) => element.key == 'requestHeader')
.isNotEmpty)
.toList();
excludeCommonSettingKeys = [
'versionExtractionRegEx',
'matchGroupToUse',
'versionDetection',
'useVersionCodeAsOSVersion',
'apkFilterRegEx',
'autoApkFilterByArch'
];
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var additionalSettingsNew =
getDefaultValuesFromFormItems(html.combinedAppSpecificSettingFormItems);
for (var s in additionalSettings.keys) {
if (additionalSettingsNew.containsKey(s)) {
additionalSettingsNew[s] = additionalSettings[s];
}
}
additionalSettingsNew['defaultPseudoVersioningMethod'] = 'partialAPKHash';
additionalSettingsNew['directAPKLink'] = true;
additionalSettings['versionDetection'] = false;
return html.getLatestAPKDetails(standardUrl, additionalSettingsNew);
}
}

View File

@ -9,7 +9,7 @@ import 'package:obtainium/providers/source_provider.dart';
class FDroid extends AppSource {
FDroid() {
host = 'f-droid.org';
hosts = ['f-droid.org'];
name = tr('fdroid');
naiveStandardVersionDetection = true;
canSearch = true;
@ -37,20 +37,22 @@ class FDroid extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExB =
RegExp('^https?://(www\\.)?$host/+[^/]+/+packages/+[^/]+');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
RegExp standardUrlRegExB = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
if (match != null) {
url =
'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.last}';
}
RegExp standardUrlRegExA =
RegExp('^https?://(www\\.)?$host/+packages/+[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase());
RegExp standardUrlRegExA = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
caseSensitive: false);
match = standardUrlRegExA.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -67,7 +69,8 @@ class FDroid extends AppSource {
String? appId = await tryInferringAppId(standardUrl);
String host = Uri.parse(standardUrl).host;
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
await sourceRequest('https://$host/api/v1/packages/$appId'),
await sourceRequest(
'https://$host/api/v1/packages/$appId', additionalSettings),
'https://$host/repo/$appId',
standardUrl,
name,
@ -84,29 +87,30 @@ class FDroid extends AppSource {
if (!hostChanged) {
try {
var res = await sourceRequest(
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml');
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
additionalSettings);
var lines = res.body.split('\n');
String author = lines
.where((l) => l.startsWith('AuthorName: '))
.first
.split(': ')
.sublist(1)
.join(': ');
details.names.author = author;
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
if (authorLines.isNotEmpty) {
details.names.author =
authorLines.first.split(': ').sublist(1).join(': ');
}
var changelogUrls = lines.where((l) => l.startsWith('Changelog: '));
if (changelogUrls.isNotEmpty) {
details.changeLog = changelogUrls.first;
details.changeLog = (await sourceRequest(details.changeLog!
.split(': ')
.sublist(1)
.join(': ')
.replaceFirst('/blob/', '/raw/')))
details.changeLog = (await sourceRequest(
details.changeLog!
.split(': ')
.sublist(1)
.join(': ')
.replaceFirst('/blob/', '/raw/'),
additionalSettings))
.body;
}
} catch (e) {
// Fail silently
}
if ((details.changeLog?.length ?? 0) > 1000) {
if ((details.changeLog?.length ?? 0) > 2048) {
details.changeLog = '${details.changeLog!.substring(0, 2048)}...';
}
}
@ -117,7 +121,7 @@ class FDroid extends AppSource {
Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) async {
Response res = await sourceRequest(
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {};
parse(res.body).querySelectorAll('.package-header').forEach((e) {

View File

@ -10,6 +10,7 @@ class FDroidRepo extends AppSource {
canSearch = true;
excludeFromMassSearch = true;
neverAutoSelect = true;
showReleaseDateAsVersionToggle = true;
additionalSourceAppSpecificSettingFormItems = [
[
@ -59,7 +60,7 @@ class FDroidRepo extends AppSource {
throw NoReleasesError();
}
url = removeQueryParamsFromUrl(standardizeUrl(url));
var res = await sourceRequest('$url/index.xml');
var res = await sourceRequest('$url/index.xml', {});
if (res.statusCode == 200) {
var body = parse(res.body);
Map<String, List<String>> results = {};
@ -117,7 +118,8 @@ class FDroidRepo extends AppSource {
throw NoReleasesError();
}
var res = await sourceRequest(
'$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}');
'$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}',
additionalSettings);
if (res.statusCode == 200) {
var body = parse(res.body);
var foundApps = body.querySelectorAll('application').where((element) {

View File

@ -14,8 +14,9 @@ import 'package:url_launcher/url_launcher_string.dart';
class GitHub extends AppSource {
GitHub() {
host = 'github.com';
hosts = ['github.com'];
appIdInferIsOptional = true;
showReleaseDateAsVersionToggle = true;
sourceConfigSettingFormItems = [
GeneratedFormTextField('github-creds',
@ -76,6 +77,10 @@ class GitHub extends AppSource {
[
GeneratedFormSwitch('dontSortReleasesList',
label: tr('dontSortReleasesList'))
],
[
GeneratedFormSwitch('useLatestAssetDateAsReleaseDate',
label: tr('useLatestAssetDateAsReleaseDate'), defaultValue: false)
]
];
@ -108,7 +113,8 @@ class GitHub extends AppSource {
for (var path in possibleBuildGradleLocations) {
try {
var res = await sourceRequest(
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path');
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
additionalSettings);
if (res.statusCode == 200) {
try {
var body = jsonDecode(res.body);
@ -149,18 +155,20 @@ class GitHub extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
var token = await getTokenIfAny(additionalSettings);
var headers = <String, String>{};
if (token != null) {
@ -203,11 +211,11 @@ class GitHub extends AppSource {
}
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
'https://api.$host';
'https://api.${hosts[0]}';
Future<String> convertStandardUrlToAPIUrl(
String standardUrl, Map<String, dynamic> additionalSettings) async =>
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://$host'.length)}';
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}';
@override
String? changeLogPageFromStandardUrl(String standardUrl) =>
@ -234,11 +242,14 @@ class GitHub extends AppSource {
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
bool dontSortReleasesList =
additionalSettings['dontSortReleasesList'] == true;
bool useLatestAssetDateAsReleaseDate =
additionalSettings['useLatestAssetDateAsReleaseDate'] == true;
dynamic latestRelease;
if (verifyLatestTag) {
var temp = requestUrl.split('?');
Response res = await sourceRequest(
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}');
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}',
additionalSettings);
if (res.statusCode != 200) {
if (onHttpErrorCode != null) {
onHttpErrorCode(res);
@ -247,7 +258,7 @@ class GitHub extends AppSource {
}
latestRelease = jsonDecode(res.body);
}
Response res = await sourceRequest(requestUrl);
Response res = await sourceRequest(requestUrl, additionalSettings);
if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>;
if (latestRelease != null) {
@ -273,10 +284,31 @@ class GitHub extends AppSource {
.toList() ??
[];
DateTime? getReleaseDateFromRelease(dynamic rel) =>
DateTime? getPublishDateFromRelease(dynamic rel) =>
rel?['published_at'] != null
? DateTime.parse(rel['published_at'])
: null;
DateTime? getNewestAssetDateFromRelease(dynamic rel) {
var t = (rel['assets'] as List<dynamic>?)
?.map((e) {
return e?['updated_at'] != null
? DateTime.parse(e['updated_at'])
: null;
})
.where((e) => e != null)
.toList();
t?.sort((a, b) => b!.compareTo(a!));
if (t?.isNotEmpty == true) {
return t!.first;
}
return null;
}
DateTime? getReleaseDateFromRelease(dynamic rel, bool useAssetDate) =>
!useAssetDate
? getPublishDateFromRelease(rel)
: getNewestAssetDateFromRelease(rel);
if (dontSortReleasesList) {
releases = releases.reversed.toList();
} else {
@ -301,8 +333,12 @@ class GitHub extends AppSource {
(nameA as String).substring(matchA!.start, matchA.end),
(nameB as String).substring(matchB!.start, matchB.end));
} else {
return (getReleaseDateFromRelease(a) ?? DateTime(1))
.compareTo(getReleaseDateFromRelease(b) ?? DateTime(0));
return (getReleaseDateFromRelease(
a, useLatestAssetDateAsReleaseDate) ??
DateTime(1))
.compareTo(getReleaseDateFromRelease(
b, useLatestAssetDateAsReleaseDate) ??
DateTime(0));
}
}
});
@ -346,11 +382,8 @@ class GitHub extends AppSource {
continue;
}
var apkUrls = getReleaseAPKUrls(releases[i]);
if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
apkUrls =
apkUrls.where((element) => reg.hasMatch(element.key)).toList();
}
apkUrls = filterApks(apkUrls, additionalSettings['apkFilterRegEx'],
additionalSettings['invertAPKFilter']);
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
continue;
}
@ -362,7 +395,8 @@ class GitHub extends AppSource {
throw NoReleasesError();
}
String? version = targetRelease['tag_name'] ?? targetRelease['name'];
DateTime? releaseDate = getReleaseDateFromRelease(targetRelease);
DateTime? releaseDate = getReleaseDateFromRelease(
targetRelease, useLatestAssetDateAsReleaseDate);
if (version == null) {
throw NoVersionError();
}
@ -424,7 +458,7 @@ class GitHub extends AppSource {
String query, String requestUrl, String rootProp,
{Function(Response)? onHttpErrorCode,
Map<String, dynamic> querySettings = const {}}) async {
Response res = await sourceRequest(requestUrl);
Response res = await sourceRequest(requestUrl, {});
if (res.statusCode == 200) {
int minStarCount = querySettings['minStarCount'] != null
? int.parse(querySettings['minStarCount'])

View File

@ -13,8 +13,9 @@ import 'package:url_launcher/url_launcher_string.dart';
class GitLab extends AppSource {
GitLab() {
host = 'gitlab.com';
hosts = ['gitlab.com'];
canSearch = true;
showReleaseDateAsVersionToggle = true;
sourceConfigSettingFormItems = [
GeneratedFormTextField('gitlab-creds',
@ -52,12 +53,14 @@ class GitLab extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
@ -81,15 +84,15 @@ class GitLab extends AppSource {
Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) async {
var url =
'https://$host/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
var res = await sourceRequest(url);
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
var res = await sourceRequest(url, {});
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var json = jsonDecode(res.body) as List<dynamic>;
Map<String, List<String>> results = {};
for (var element in json) {
results['https://$host/${element['path_with_namespace']}'] = [
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
element['name_with_namespace'],
element['description'] ?? tr('noDescription')
];
@ -113,7 +116,8 @@ class GitLab extends AppSource {
if (PAT != null) {
var names = GitHub().getAppNames(standardUrl);
Response res = await sourceRequest(
'https://$host/api/v4/projects/${names.author}%2F${names.name}/releases?private_token=$PAT');
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/releases?private_token=$PAT',
additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
@ -148,7 +152,8 @@ class GitLab extends AppSource {
releaseDate: releaseDate);
});
} else {
Response res = await sourceRequest('$standardUrl/-/tags?format=atom');
Response res = await sourceRequest(
'$standardUrl/-/tags?format=atom', additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}

View File

@ -19,6 +19,8 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
.toList();
String absoluteUrl;
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
absoluteUrl = '${referenceAbsoluteUrl.origin}$ambiguousUrl';
} else if (currPathSegments.isEmpty) {
absoluteUrl = '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
} else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
absoluteUrl =
@ -106,11 +108,7 @@ class HTML extends AppSource {
[
GeneratedFormSwitch('versionExtractWholePage',
label: tr('versionExtractWholePage'))
],
[
GeneratedFormSwitch('supportFixedAPKURL',
defaultValue: true, label: tr('supportFixedAPKURL')),
],
]
];
var commonFormItems = [
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
@ -139,20 +137,72 @@ class HTML extends AppSource {
],
finalStepFormitems[0],
...commonFormItems,
...finalStepFormitems.sublist(1)
...finalStepFormitems.sublist(1),
[
GeneratedFormSubForm(
'requestHeader',
[
[
GeneratedFormTextField('requestHeader',
label: tr('requestHeader'),
required: false,
additionalValidators: [
(value) {
if ((value ?? 'empty:valid')
.split(':')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.length <
2) {
return tr('invalidInput');
}
return null;
}
])
]
],
label: tr('requestHeader'),
defaultValue: [
{
'requestHeader':
'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
}
])
],
[
GeneratedFormDropdown(
'defaultPseudoVersioningMethod',
[
MapEntry('partialAPKHash', tr('partialAPKHash')),
MapEntry('APKLinkHash', tr('APKLinkHash'))
],
label: tr('defaultPseudoVersioningMethod'),
defaultValue: 'partialAPKHash')
]
];
overrideVersionDetectionFormDefault('noVersionDetection',
disableStandard: false, disableRelDate: true);
}
@override
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
return {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
};
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
if (additionalSettings.isNotEmpty) {
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
additionalSettings['requestHeader'] = [];
}
additionalSettings['requestHeader'] = additionalSettings['requestHeader']
.where((l) => l['requestHeader'].isNotEmpty == true)
.toList();
Map<String, String> requestHeaders = {};
for (int i = 0; i < (additionalSettings['requestHeader'].length); i++) {
var temp =
(additionalSettings['requestHeader'][i]['requestHeader'] as String)
.split(':');
requestHeaders[temp[0].trim()] = temp.sublist(1).join(':').trim();
}
return requestHeaders;
}
return null;
}
@override
@ -233,7 +283,8 @@ class HTML extends AppSource {
.where((l) => l['customLinkFilterRegex'].isNotEmpty == true)
.toList();
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
var intLinks = await grabLinksCommon(await sourceRequest(currentUrl),
var intLinks = await grabLinksCommon(
await sourceRequest(currentUrl, additionalSettings),
additionalSettings['intermediateLink'][i]);
if (intLinks.isEmpty) {
throw NoReleasesError();
@ -241,30 +292,34 @@ class HTML extends AppSource {
currentUrl = intLinks.last.key;
}
}
var uri = Uri.parse(currentUrl);
Response res = await sourceRequest(currentUrl);
var links = await grabLinksCommon(res, additionalSettings);
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
links = links.where((element) => reg.hasMatch(element.key)).toList();
}
if (links.isEmpty) {
throw NoReleasesError();
List<MapEntry<String, String>> links = [];
String versionExtractionWholePageString = currentUrl;
if (additionalSettings['directAPKLink'] != true) {
Response res = await sourceRequest(currentUrl, additionalSettings);
versionExtractionWholePageString =
res.body.split('\r\n').join('\n').split('\n').join('\\n');
links = await grabLinksCommon(res, additionalSettings);
links = filterApks(links, additionalSettings['apkFilterRegEx'],
additionalSettings['invertAPKFilter']);
if (links.isEmpty) {
throw NoReleasesError();
}
} else {
links = [MapEntry(currentUrl, currentUrl)];
}
var rel = links.last.key;
String? version;
if (additionalSettings['supportFixedAPKURL'] != true) {
version = rel.hashCode.toString();
}
version = extractVersion(
additionalSettings['versionExtractionRegEx'] as String?,
additionalSettings['matchGroupToUse'] as String?,
additionalSettings['versionExtractWholePage'] == true
? res.body.split('\r\n').join('\n').split('\n').join('\\n')
? versionExtractionWholePageString
: rel);
version ??= (await checkDownloadHash(rel)).toString();
version ??=
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
? rel.hashCode.toString()
: (await checkPartialDownloadHashDynamc(rel)).toString();
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
AppNames(uri.host, tr('app')));
}

View File

@ -6,26 +6,30 @@ import 'package:obtainium/providers/source_provider.dart';
class HuaweiAppGallery extends AppSource {
HuaweiAppGallery() {
name = 'Huawei AppGallery';
host = 'appgallery.huawei.com';
overrideVersionDetectionFormDefault('releaseDateAsVersion',
disableStandard: true);
hosts = ['appgallery.huawei.com'];
versionDetectionDisallowed = true;
showReleaseDateAsVersionToggle = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/app/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/app/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
getDlUrl(String standardUrl) =>
'https://${host!.replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}';
'https://${hosts[0].replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}';
requestAppdlRedirect(String dlUrl) async {
Response res = await sourceRequest(dlUrl, followRedirects: false);
requestAppdlRedirect(
String dlUrl, Map<String, dynamic> additionalSettings) async {
Response res =
await sourceRequest(dlUrl, additionalSettings, followRedirects: false);
if (res.statusCode == 200 ||
res.statusCode == 302 ||
res.statusCode == 304) {
@ -52,7 +56,7 @@ class HuaweiAppGallery extends AppSource {
Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async {
String dlUrl = getDlUrl(standardUrl);
Response res = await requestAppdlRedirect(dlUrl);
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
return res.headers['location'] != null
? appIdFromRedirectDlUrl(res.headers['location']!)
: null;
@ -64,7 +68,7 @@ class HuaweiAppGallery extends AppSource {
Map<String, dynamic> additionalSettings,
) async {
String dlUrl = getDlUrl(standardUrl);
Response res = await requestAppdlRedirect(dlUrl);
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
if (res.headers['location'] == null) {
throw NoReleasesError();
}

View File

@ -6,7 +6,7 @@ class IzzyOnDroid extends AppSource {
late FDroid fd;
IzzyOnDroid() {
host = 'izzysoft.de';
hosts = ['izzysoft.de'];
fd = FDroid();
additionalSourceAppSpecificSettingFormItems =
fd.additionalSourceAppSpecificSettingFormItems;
@ -15,17 +15,20 @@ class IzzyOnDroid extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExA = RegExp('^https?://android.$host/repo/apk/[^/]+');
RegExpMatch? match = standardUrlRegExA.firstMatch(url.toLowerCase());
RegExp standardUrlRegExA = RegExp(
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
if (match == null) {
RegExp standardUrlRegExB =
RegExp('^https?://apt.$host/fdroid/index/apk/[^/]+');
match = standardUrlRegExB.firstMatch(url.toLowerCase());
RegExp standardUrlRegExB = RegExp(
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
caseSensitive: false);
match = standardUrlRegExB.firstMatch(url);
}
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -42,7 +45,8 @@ class IzzyOnDroid extends AppSource {
String? appId = await tryInferringAppId(standardUrl);
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
await sourceRequest(
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
additionalSettings),
'https://android.izzysoft.de/frepo/$appId',
standardUrl,
name,

View File

@ -6,8 +6,9 @@ import 'package:obtainium/providers/source_provider.dart';
class Jenkins extends AppSource {
Jenkins() {
overrideVersionDetectionFormDefault('releaseDateAsVersion',
disableStandard: true);
versionDetectionDisallowed = true;
neverAutoSelect = true;
showReleaseDateAsVersionToggle = true;
}
String trimJobUrl(String url) {
@ -16,7 +17,7 @@ class Jenkins extends AppSource {
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -29,8 +30,8 @@ class Jenkins extends AppSource {
Map<String, dynamic> additionalSettings,
) async {
standardUrl = trimJobUrl(standardUrl);
Response res =
await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
Response res = await sourceRequest(
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
var releaseDate = json['timestamp'] == null

View File

@ -6,17 +6,19 @@ import 'package:obtainium/providers/source_provider.dart';
class Mullvad extends AppSource {
Mullvad() {
host = 'mullvad.net';
hosts = ['mullvad.net'];
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -28,7 +30,8 @@ class Mullvad extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await sourceRequest('$standardUrl/en/download/android');
Response res = await sourceRequest(
'$standardUrl/en/download/android', additionalSettings);
if (res.statusCode == 200) {
var versions = parse(res.body)
.querySelectorAll('p')

View File

@ -5,18 +5,20 @@ import 'package:obtainium/providers/source_provider.dart';
class NeutronCode extends AppSource {
NeutronCode() {
host = 'neutroncode.com';
hosts = ['neutroncode.com'];
showReleaseDateAsVersionToggle = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx =
RegExp('^https?://(www\\.)?$host/downloads/file/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -79,7 +81,7 @@ class NeutronCode extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await sourceRequest(standardUrl);
Response res = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode == 200) {
var http = parse(res.body);
var name = http.querySelector('.pd-title')?.innerHtml;
@ -92,7 +94,7 @@ class NeutronCode extends AppSource {
if (version == null) {
throw NoVersionError();
}
String? apkUrl = 'https://$host/download/$filename';
String? apkUrl = 'https://${hosts[0]}/download/$filename';
var dateStringOriginal =
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
var dateString = dateStringOriginal != null

View File

@ -5,12 +5,12 @@ import 'package:obtainium/providers/source_provider.dart';
class Signal extends AppSource {
Signal() {
host = 'signal.org';
hosts = ['signal.org'];
}
@override
String sourceSpecificStandardizeURL(String url) {
return 'https://$host';
return 'https://${hosts[0]}';
}
@override
@ -18,8 +18,8 @@ class Signal extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res =
await sourceRequest('https://updates.$host/android/latest.json');
Response res = await sourceRequest(
'https://updates.${hosts[0]}/android/latest.json', additionalSettings);
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
String? apkUrl = json['url'];

View File

@ -5,24 +5,27 @@ import 'package:obtainium/providers/source_provider.dart';
class SourceForge extends AppSource {
SourceForge() {
host = 'sourceforge.net';
hosts = ['sourceforge.net'];
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExB = RegExp('^https?://(www\\.)?$host/p/[^/]+');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
RegExp standardUrlRegExB = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/p/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
if (match != null) {
url =
'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}';
'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}';
}
RegExp standardUrlRegExA =
RegExp('^https?://(www\\.)?$host/projects/[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase());
RegExp standardUrlRegExA = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/projects/[^/]+',
caseSensitive: false);
match = standardUrlRegExA.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -30,7 +33,8 @@ class SourceForge extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await sourceRequest('$standardUrl/rss?path=/');
Response res =
await sourceRequest('$standardUrl/rss?path=/', additionalSettings);
if (res.statusCode == 200) {
var parsedHtml = parse(res.body);
var allDownloadLinks =

View File

@ -8,7 +8,8 @@ import 'package:easy_localization/easy_localization.dart';
class SourceHut extends AppSource {
SourceHut() {
host = 'git.sr.ht';
hosts = ['git.sr.ht'];
showReleaseDateAsVersionToggle = true;
additionalSourceAppSpecificSettingFormItems = [
[
@ -20,12 +21,14 @@ class SourceHut extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
return match.group(0)!;
}
@override
@ -40,7 +43,8 @@ class SourceHut extends AppSource {
String appName = standardUri.pathSegments.last;
bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'] == true;
Response res = await sourceRequest('$standardUrl/refs/rss.xml');
Response res =
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
if (res.statusCode == 200) {
var parsedHtml = parse(res.body);
List<APKDetails> apkDetailsList = [];
@ -69,7 +73,7 @@ class SourceHut extends AppSource {
} catch (e) {
// ignore
}
var res2 = await sourceRequest(releasePage);
var res2 = await sourceRequest(releasePage, additionalSettings);
List<MapEntry<String, String>> apkUrls = [];
if (res2.statusCode == 200) {
apkUrls = getApkUrlsFromUrls(parse(res2.body)

View File

@ -7,7 +7,7 @@ import 'package:obtainium/providers/source_provider.dart';
class SteamMobile extends AppSource {
SteamMobile() {
host = 'store.steampowered.com';
hosts = ['store.steampowered.com'];
name = tr('steam');
additionalSourceAppSpecificSettingFormItems = [
[
@ -21,7 +21,7 @@ class SteamMobile extends AppSource {
@override
String sourceSpecificStandardizeURL(String url) {
return 'https://$host';
return 'https://${hosts[0]}';
}
@override
@ -29,7 +29,8 @@ class SteamMobile extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await sourceRequest('https://$host/mobile');
Response res =
await sourceRequest('https://${hosts[0]}/mobile', additionalSettings);
if (res.statusCode == 200) {
var apkNamePrefix = additionalSettings['app'] as String?;
if (apkNamePrefix == null) {

View File

@ -6,13 +6,13 @@ import 'package:obtainium/providers/source_provider.dart';
class TelegramApp extends AppSource {
TelegramApp() {
host = 'telegram.org';
hosts = ['telegram.org'];
name = 'Telegram ${tr('app')}';
}
@override
String sourceSpecificStandardizeURL(String url) {
return 'https://$host';
return 'https://${hosts[0]}';
}
@override
@ -20,7 +20,8 @@ class TelegramApp extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await sourceRequest('https://t.me/s/TAndroidAPK');
Response res =
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
if (res.statusCode == 200) {
var http = parse(res.body);
var messages =

View File

@ -6,29 +6,34 @@ import 'package:obtainium/providers/source_provider.dart';
class Uptodown extends AppSource {
Uptodown() {
host = 'uptodown.com';
hosts = ['uptodown.com'];
allowSubDomains = true;
naiveStandardVersionDetection = true;
showReleaseDateAsVersionToggle = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegEx = RegExp(
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return '${url.substring(0, match.end)}/android/download';
return '${match.group(0)!}/android/download';
}
@override
Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async {
return (await getAppDetailsFromPage(standardUrl))['appId'];
return (await getAppDetailsFromPage(
standardUrl, additionalSettings))['appId'];
}
Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async {
var res = await sourceRequest(standardUrl);
Future<Map<String, String?>> getAppDetailsFromPage(
String standardUrl, Map<String, dynamic> additionalSettings) async {
var res = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
@ -56,7 +61,8 @@ class Uptodown extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var appDetails = await getAppDetailsFromPage(standardUrl);
var appDetails =
await getAppDetailsFromPage(standardUrl, additionalSettings);
var version = appDetails['version'];
var apkUrl = appDetails['apkUrl'];
var appId = appDetails['appId'];
@ -82,9 +88,9 @@ class Uptodown extends AppSource {
}
@override
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
var res = await sourceRequest(apkUrl);
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
Map<String, dynamic> additionalSettings) async {
var res = await sourceRequest(apkUrl, additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
@ -94,6 +100,6 @@ class Uptodown extends AppSource {
if (finalUrlKey == null) {
throw NoAPKError();
}
return 'https://dw.$host/dwn/$finalUrlKey';
return 'https://dw.${hosts[0]}/dwn/$finalUrlKey';
}
}

View File

@ -1,31 +1,33 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class VLC extends AppSource {
VLC() {
host = 'videolan.org';
hosts = ['videolan.org'];
}
get dwUrlBase => 'https://get.$host/vlc-android/';
get dwUrlBase => 'https://get.${hosts[0]}/vlc-android/';
@override
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) =>
HTML().getRequestHeaders(
additionalSettings: additionalSettings,
forAPKDownload: forAPKDownload);
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
return {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
};
}
@override
String sourceSpecificStandardizeURL(String url) {
return 'https://$host';
return 'https://${hosts[0]}';
}
Future<String?> getLatestVersion(String standardUrl) async {
Response res = await sourceRequest(dwUrlBase);
Future<String?> getLatestVersion(
String standardUrl, Map<String, dynamic> additionalSettings) async {
Response res = await sourceRequest(dwUrlBase, additionalSettings);
if (res.statusCode == 200) {
var dwLinks = parse(res.body)
.querySelectorAll('a')
@ -77,9 +79,9 @@ class VLC extends AppSource {
}
@override
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
Response res = await sourceRequest(apkUrl);
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
Map<String, dynamic> additionalSettings) async {
Response res = await sourceRequest(apkUrl, additionalSettings);
if (res.statusCode == 200) {
String? apkUrl =
parse(res.body).querySelector('#alt_link')?.attributes['href'];

View File

@ -5,20 +5,20 @@ import 'package:obtainium/providers/source_provider.dart';
class WhatsApp extends AppSource {
WhatsApp() {
host = 'whatsapp.com';
overrideVersionDetectionFormDefault('noVersionDetection',
disableStandard: true, disableRelDate: true);
hosts = ['whatsapp.com'];
versionDetectionDisallowed = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
return 'https://$host';
return 'https://${hosts[0]}';
}
@override
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
Response res = await sourceRequest('$standardUrl/android');
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
Map<String, dynamic> additionalSettings) async {
Response res =
await sourceRequest('$standardUrl/android', additionalSettings);
if (res.statusCode == 200) {
var targetLinks = parse(res.body)
.querySelectorAll('a')
@ -42,8 +42,8 @@ class WhatsApp extends AppSource {
) async {
// This is a CDN link that is consistent per version
// But it has query params that change constantly
Uri apkUri =
Uri.parse(await apkUrlPrefetchModifier(standardUrl, standardUrl));
Uri apkUri = Uri.parse(await apkUrlPrefetchModifier(
standardUrl, standardUrl, additionalSettings));
var unusableApkUrl = '${apkUri.origin}/${apkUri.path}';
// So we use the param-less URL is a pseudo-version to add the app and check for updates
// See #357 for why we can't scrape the version number directly

View File

@ -13,6 +13,7 @@ abstract class GeneratedFormItem {
late dynamic defaultValue;
List<dynamic> additionalValidators;
dynamic ensureType(dynamic val);
GeneratedFormItem clone();
GeneratedFormItem(this.key,
{this.label = 'Input',
@ -44,6 +45,20 @@ class GeneratedFormTextField extends GeneratedFormItem {
String ensureType(val) {
return val.toString();
}
@override
GeneratedFormTextField clone() {
return GeneratedFormTextField(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: List.from(additionalValidators),
required: required,
max: max,
hint: hint,
password: password,
textInputType: textInputType);
}
}
class GeneratedFormDropdown extends GeneratedFormItem {
@ -64,14 +79,31 @@ class GeneratedFormDropdown extends GeneratedFormItem {
String ensureType(val) {
return val.toString();
}
@override
GeneratedFormDropdown clone() {
return GeneratedFormDropdown(
key,
opts?.map((e) => MapEntry(e.key, e.value)).toList(),
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
disabledOptKeys:
disabledOptKeys != null ? List.from(disabledOptKeys!) : null,
additionalValidators: List.from(additionalValidators),
);
}
}
class GeneratedFormSwitch extends GeneratedFormItem {
bool disabled = false;
GeneratedFormSwitch(
super.key, {
super.label,
super.belowWidgets,
bool super.defaultValue = false,
bool disabled = false,
List<String? Function(bool value)> super.additionalValidators = const [],
});
@ -79,6 +111,16 @@ class GeneratedFormSwitch extends GeneratedFormItem {
bool ensureType(val) {
return val == true || val == 'true';
}
@override
GeneratedFormSwitch clone() {
return GeneratedFormSwitch(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
disabled: false,
additionalValidators: List.from(additionalValidators));
}
}
class GeneratedFormTagInput extends GeneratedFormItem {
@ -103,6 +145,20 @@ class GeneratedFormTagInput extends GeneratedFormItem {
Map<String, MapEntry<int, bool>> ensureType(val) {
return val is Map<String, MapEntry<int, bool>> ? val : {};
}
@override
GeneratedFormTagInput clone() {
return GeneratedFormTagInput(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: List.from(additionalValidators),
deleteConfirmationMessage: deleteConfirmationMessage,
singleSelect: singleSelect,
alignment: alignment,
emptyMessage: emptyMessage,
showLabelWhenNotEmpty: showLabelWhenNotEmpty);
}
}
typedef OnValueChanges = void Function(
@ -119,6 +175,19 @@ class GeneratedForm extends StatefulWidget {
State<GeneratedForm> createState() => _GeneratedFormState();
}
List<List<GeneratedFormItem>> cloneFormItems(
List<List<GeneratedFormItem>> items) {
List<List<GeneratedFormItem>> clonedItems = [];
for (var row in items) {
List<GeneratedFormItem> clonedRow = [];
for (var it in row) {
clonedRow.add(it.clone());
}
clonedItems.add(clonedRow);
}
return clonedItems;
}
class GeneratedFormSubForm extends GeneratedFormItem {
final List<List<GeneratedFormItem>> items;
@ -129,6 +198,12 @@ class GeneratedFormSubForm extends GeneratedFormItem {
ensureType(val) {
return val; // Not easy to validate List<Map<String, dynamic>>
}
@override
GeneratedFormSubForm clone() {
return GeneratedFormSubForm(key, cloneFormItems(items),
label: label, belowWidgets: belowWidgets, defaultValue: defaultValue);
}
}
// Generates a color in the HSLuv (Pastel) color space
@ -297,15 +372,51 @@ class _GeneratedFormState extends State<GeneratedForm> {
),
Switch(
value: values[fieldKey],
onChanged: (value) {
setState(() {
values[fieldKey] = value;
someValueChanged();
});
})
onChanged:
(widget.items[r][e] as GeneratedFormSwitch).disabled
? null
: (value) {
setState(() {
values[fieldKey] = value;
someValueChanged();
});
})
],
);
} else if (widget.items[r][e] is GeneratedFormTagInput) {
onAddPressed() {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: widget.items[r][e].label,
items: [
[GeneratedFormTextField('label', label: tr('label'))]
]);
}).then((value) {
String? label = value?['label'];
if (label != null) {
setState(() {
var temp =
values[fieldKey] as Map<String, MapEntry<int, bool>>?;
temp ??= {};
if (temp[label] == null) {
var singleSelect =
(widget.items[r][e] as GeneratedFormTagInput)
.singleSelect;
var someSelected = temp.entries
.where((element) => element.value.value)
.isNotEmpty;
temp[label] = MapEntry(generateRandomLightColor().value,
!(someSelected && singleSelect));
values[fieldKey] = temp;
someValueChanged();
}
});
}
});
}
formInputs[r][e] =
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
@ -331,14 +442,14 @@ class _GeneratedFormState extends State<GeneratedForm> {
(widget.items[r][e] as GeneratedFormTagInput).alignment,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.isEmpty ==
true
? Text(
(widget.items[r][e] as GeneratedFormTagInput)
.emptyMessage,
)
: const SizedBox.shrink(),
// (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
// ?.isEmpty ==
// true
// ? Text(
// (widget.items[r][e] as GeneratedFormTagInput)
// .emptyMessage,
// )
// : const SizedBox.shrink(),
...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.entries
.map((e2) {
@ -462,62 +573,37 @@ class _GeneratedFormState extends State<GeneratedForm> {
tooltip: tr('remove'),
))
: const SizedBox.shrink(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton(
onPressed: () {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: widget.items[r][e].label,
items: [
[
GeneratedFormTextField('label',
label: tr('label'))
]
]);
}).then((value) {
String? label = value?['label'];
if (label != null) {
setState(() {
var temp = values[fieldKey]
as Map<String, MapEntry<int, bool>>?;
temp ??= {};
if (temp[label] == null) {
var singleSelect = (widget.items[r][e]
as GeneratedFormTagInput)
.singleSelect;
var someSelected = temp.entries
.where((element) => element.value.value)
.isNotEmpty;
temp[label] = MapEntry(
generateRandomLightColor().value,
!(someSelected && singleSelect));
values[fieldKey] = temp;
someValueChanged();
}
});
}
});
},
icon: const Icon(Icons.add),
visualDensity: VisualDensity.compact,
tooltip: tr('add'),
)),
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.isEmpty ==
true
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: TextButton.icon(
onPressed: onAddPressed,
icon: const Icon(Icons.add),
label: Text(
(widget.items[r][e] as GeneratedFormTagInput)
.label),
))
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton(
onPressed: onAddPressed,
icon: const Icon(Icons.add),
visualDensity: VisualDensity.compact,
tooltip: tr('add'),
)),
],
)
]);
} else if (widget.items[r][e] is GeneratedFormSubForm) {
List<Widget> subformColumn = [];
var compact = (widget.items[r][e] as GeneratedFormSubForm)
.items
.length ==
1 &&
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
for (int i = 0; i < values[fieldKey].length; i++) {
var items = (widget.items[r][e] as GeneratedFormSubForm)
.items
.map((x) => x.map((y) {
y.defaultValue = values[fieldKey]?[i]?[y.key];
return y;
}).toList())
.toList();
var internalFormKey = ValueKey(generateRandomNumber(
values[fieldKey].length,
seed2: i,
@ -525,18 +611,28 @@ class _GeneratedFormState extends State<GeneratedForm> {
subformColumn.add(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const SizedBox(
height: 16,
),
Text(
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
style: const TextStyle(fontWeight: FontWeight.bold),
),
if (!compact)
const SizedBox(
height: 16,
),
if (!compact)
Text(
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
style: const TextStyle(fontWeight: FontWeight.bold),
),
GeneratedForm(
key: internalFormKey,
items: items,
items: cloneFormItems(
(widget.items[r][e] as GeneratedFormSubForm).items)
.map((x) => x.map((y) {
y.defaultValue = values[fieldKey]?[i]?[y.key];
y.key = '${y.key.toString()},$internalFormKey';
return y;
}).toList())
.toList(),
onValueChanges: (values, valid, isBuilding) {
values = values.map(
(key, value) => MapEntry(key.split(',')[0], value));
if (valid) {
this.values[fieldKey]?[i] = values;
}
@ -567,13 +663,12 @@ class _GeneratedFormState extends State<GeneratedForm> {
Icons.delete_outline_rounded,
))
],
),
)
],
));
}
subformColumn.add(Padding(
padding: EdgeInsets.only(
bottom: values[fieldKey].length > 0 ? 24 : 0, top: 8),
padding: const EdgeInsets.only(bottom: 0, top: 8),
child: Row(
children: [
Expanded(
@ -591,9 +686,6 @@ class _GeneratedFormState extends State<GeneratedForm> {
],
),
));
if (values[fieldKey].length > 0) {
subformColumn.add(const Divider());
}
formInputs[r][e] = Column(children: subformColumn);
}
}

View File

@ -19,12 +19,6 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.15.7';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
const int bgUpdateCheckAlarmId = 666;
List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('en'), 'English'),
MapEntry(Locale('zh'), '简体中文'),
@ -146,6 +140,7 @@ class _ObtainiumState extends State<Obtainium> {
BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
@ -176,20 +171,31 @@ class _ObtainiumState extends State<Obtainium> {
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
Permission.notification.request();
if (!fdroid) {
appsProvider.saveApps([
App(
obtainiumId,
'https://github.com/ImranR98/Obtainium',
'ImranR98',
'Obtainium',
currentReleaseTag,
currentReleaseTag,
[],
0,
{'includePrereleases': true},
null,
false)
], onlyIfExists: false);
getInstalledInfo(obtainiumId).then((value) {
if (value?.versionName != null) {
appsProvider.saveApps([
App(
obtainiumId,
obtainiumUrl,
'ImranR98',
'Obtainium',
value!.versionName,
value.versionName!,
[],
0,
{
'includePrereleases': true,
'versionDetection': true,
'apkFilterRegEx': 'fdroid',
'invertAPKFilter': true
},
null,
false)
], onlyIfExists: false);
}
}).catchError((err) {
print(err);
});
}
}
if (!supportedLocales

View File

@ -18,7 +18,7 @@ class GitHubStars implements MassAppUrlSource {
Response res = await get(
Uri.parse(
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
headers: await GitHub().getRequestHeaders());
headers: await GitHub().getRequestHeaders({}));
if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body) as List<dynamic>)) {

View File

@ -59,7 +59,9 @@ class AddAppPageState extends State<AddAppPage> {
if (updateUrlInput) {
urlInputKey++;
}
var prevHost = pickedSource?.host;
var prevHost = pickedSource?.hosts.isNotEmpty == true
? pickedSource?.hosts[0]
: null;
try {
var naturalSource =
valid ? sourceProvider.getSource(userInput) : null;
@ -77,7 +79,7 @@ class AddAppPageState extends State<AddAppPage> {
overrideSource: pickedSourceOverride)
: null;
if (pickedSource.runtimeType != source.runtimeType ||
(prevHost != null && prevHost != source?.host)) {
(prevHost != null && prevHost != source?.hosts[0])) {
pickedSource = source;
additionalSettings = source != null
? getDefaultValuesFromFormItems(
@ -133,8 +135,7 @@ class AddAppPageState extends State<AddAppPage> {
getReleaseDateAsVersionConfirmationIfNeeded(
bool userPickedTrackOnly) async {
return (!(additionalSettings['versionDetection'] ==
'releaseDateAsVersion' &&
return (!(additionalSettings['releaseDateAsVersion'] == true &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
@ -190,8 +191,7 @@ class AddAppPageState extends State<AddAppPage> {
throw ObtainiumError(tr('appAlreadyAdded'));
}
if (app.additionalSettings['trackOnly'] == true ||
app.additionalSettings['versionDetection'] !=
'standardVersionDetection') {
app.additionalSettings['versionDetection'] != true) {
app.installedVersion = app.latestVersion;
}
app.categories = pickedCategories;
@ -496,36 +496,61 @@ class AddAppPageState extends State<AddAppPage> {
],
);
Widget getSourcesListWidget() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
Widget getSourcesListWidget() => Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
tr('supportedSources'),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(
height: 16,
),
...sourceProvider.sources.map((e) => GestureDetector(
onTap: e.host != null
? () {
launchUrlString('https://${e.host}',
mode: LaunchMode.externalApplication);
}
: null,
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
return GeneratedFormModal(
singleNullReturnButton: tr('ok'),
title: tr('supportedSources'),
items: const [],
additionalWidgets: [
...sourceProvider.sources.map(
(e) => Padding(
padding:
const EdgeInsets.symmetric(vertical: 4),
child: GestureDetector(
onTap: e.hosts.isNotEmpty
? () {
launchUrlString(
'https://${e.hosts[0]}',
mode: LaunchMode
.externalApplication);
}
: null,
child: Text(
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
style: TextStyle(
decoration: e.hosts.isNotEmpty
? TextDecoration.underline
: TextDecoration.none),
))),
)
],
);
},
);
},
child: Text(
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
style: TextStyle(
decoration: e.host != null
? TextDecoration.underline
: TextDecoration.none,
tr('supportedSources'),
style: const TextStyle(
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic),
)))
]);
))
],
),
);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
bottomNavigationBar:
pickedSource == null ? getSourcesListWidget() : null,
body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
CustomAppBar(title: tr('addApp')),
SliverToBoxAdapter(
@ -557,18 +582,7 @@ class AddAppPageState extends State<AddAppPage> {
: const SizedBox();
},
future: pickedSource?.getSourceNote()),
SizedBox(
height: pickedSource != null ? 16 : 96,
),
if (pickedSource != null) getAdditionalOptsCol(),
if (pickedSource == null)
const Divider(
height: 48,
),
if (pickedSource == null) getSourcesListWidget(),
SizedBox(
height: pickedSource != null ? 8 : 2,
),
])),
)
]));

View File

@ -1,7 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
@ -29,8 +28,18 @@ class _AppPageState extends State<AppPage> {
Widget build(BuildContext context) {
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
getUpdate(String id) {
appsProvider.checkUpdate(id).catchError((e) {
getUpdate(String id, {bool resetVersion = false}) {
appsProvider.checkUpdate(id).then((e) {
if (resetVersion) {
appsProvider.apps[id]?.app.additionalSettings['versionDetection'] =
true;
if (appsProvider.apps[id]?.app.installedVersion != null) {
appsProvider.apps[id]?.app.installedVersion =
appsProvider.apps[id]?.app.latestVersion;
}
appsProvider.saveApps([appsProvider.apps[id]!.app]);
}
}).catchError((e) {
showError(e, context);
return null;
});
@ -54,122 +63,113 @@ class _AppPageState extends State<AppPage> {
var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
bool isVersionDetectionStandard =
app?.app.additionalSettings['versionDetection'] ==
'standardVersionDetection';
app?.app.additionalSettings['versionDetection'] == true;
bool installedVersionIsEstimate = trackOnly ||
(app?.app.installedVersion != null &&
app?.app.additionalSettings['versionDetection'] ==
'noVersionDetection');
app?.app.additionalSettings['versionDetection'] != true);
getInfoColumn() => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GestureDetector(
onTap: () {
if (app?.app.url != null) {
launchUrlString(app?.app.url ?? '',
mode: LaunchMode.externalApplication);
}
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(tr('copiedToClipboard')),
));
},
child: Text(
app?.app.url ?? '',
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic,
fontSize: 12),
)),
const SizedBox(
height: 32,
),
Column(
getInfoColumn() {
String versionLines = '';
bool installed = app?.app.installedVersion != null;
bool upToDate = app?.app.installedVersion == app?.app.latestVersion;
if (installed) {
versionLines = '${app?.app.installedVersion} ${tr('installed')}';
if (upToDate) {
versionLines += '/${tr('latest')}';
}
} else {
versionLines = tr('notInstalled');
}
if (!upToDate) {
versionLines += '\n${app?.app.latestVersion} ${tr('latest')}';
}
String infoLines = tr('lastUpdateCheckX', args: [
app?.app.lastUpdateCheck == null
? tr('never')
: '${app?.app.lastUpdateCheck?.toLocal()}'
]);
if (trackOnly) {
infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines';
}
if (installedVersionIsEstimate) {
infoLines = '${tr('pseudoVersionInUse')}\n$infoLines';
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
child: Column(
children: [
Text(
'${tr('latestVersionX', args: [
app?.app.latestVersion ?? tr('unknown')
])}\n${tr('installedVersionX', args: [
app?.app.installedVersion ?? tr('none')
])}${installedVersionIsEstimate ? '\n${tr('estimateInBrackets')}' : ''}',
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodyLarge!,
const SizedBox(
height: 8,
),
Text(versionLines,
textAlign: TextAlign.start,
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(fontWeight: FontWeight.bold)),
app?.app.releaseDate == null
? const SizedBox.shrink()
: Text(
app!.app.releaseDate.toString(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(
height: 8,
),
],
),
if (app?.app.installedVersion != null &&
!isVersionDetectionStandard)
Column(
children: [
const SizedBox(
height: 16,
),
Text(
'${trackOnly ? '${tr('xIsTrackOnly', args: [
tr('app')
])}\n' : ''}${tr('noVersionDetection')}',
style: Theme.of(context).textTheme.labelSmall,
),
Text(
infoLines,
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
),
const SizedBox(
height: 48,
),
CategoryEditorSelector(
alignment: WrapAlignment.center,
preselected: app?.app.categories != null
? app!.app.categories.toSet()
: {},
onSelected: (categories) {
if (app != null) {
app.app.categories = categories;
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,
)
],
),
const SizedBox(
height: 32,
),
Text(
tr('lastUpdateCheckX', args: [
app?.app.lastUpdateCheck == null
? tr('never')
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
]),
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
),
const SizedBox(
height: 48,
),
CategoryEditorSelector(
alignment: WrapAlignment.center,
preselected: app?.app.categories != null
? app!.app.categories.toSet()
: {},
onSelected: (categories) {
if (app != null) {
app.app.categories = categories;
appsProvider.saveApps([app.app]);
}
}),
if (app?.app.additionalSettings['about'] is String &&
app?.app.additionalSettings['about'].isNotEmpty)
Column(
children: [
const SizedBox(
height: 48,
style: const TextStyle(fontStyle: FontStyle.italic),
),
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),
),
)
],
),
],
);
)
],
),
],
);
}
getFullInfoColumn() => Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -196,11 +196,26 @@ class _AppPageState extends State<AppPage> {
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayLarge,
),
Text(
tr('byX', args: [app?.app.author ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
GestureDetector(
onTap: () {
if (app?.app.url != null) {
launchUrlString(app?.app.url ?? '',
mode: LaunchMode.externalApplication);
}
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(tr('copiedToClipboard')),
));
},
child: Text(
tr('byX', args: [app?.app.author ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic),
)),
const SizedBox(
height: 8,
),
@ -209,16 +224,6 @@ class _AppPageState extends State<AppPage> {
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
app?.app.releaseDate == null
? const SizedBox.shrink()
: Text(
app!.app.releaseDate.toString(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(
height: 32,
),
getInfoColumn(),
const SizedBox(height: 150)
],
@ -287,25 +292,6 @@ class _AppPageState extends State<AppPage> {
return row;
}).toList();
items = items.map((row) {
row = row.map((e) {
if (e.key == 'versionDetection' && e is GeneratedFormDropdown) {
e.disabledOptKeys ??= [];
if (app?.app.installedVersion != null &&
app?.app.additionalSettings['versionDetection'] !=
'releaseDateAsVersion' &&
!appsProvider.isVersionDetectionPossible(app)) {
e.disabledOptKeys!.add('standardVersionDetection');
}
if (app?.app.releaseDate == null) {
e.disabledOptKeys!.add('releaseDateAsVersion');
}
}
return e;
}).toList();
return row;
}).toList();
return GeneratedFormModal(
title: tr('additionalOptions'), items: items);
});
@ -320,26 +306,34 @@ class _AppPageState extends State<AppPage> {
// ignore: use_build_context_synchronously
showMessage(tr('appsFromSourceAreTrackOnly'), context);
}
if (app.app.additionalSettings['versionDetection'] ==
'releaseDateAsVersion') {
if (originalSettings['versionDetection'] != 'releaseDateAsVersion') {
if (app.app.releaseDate != null) {
bool isUpdated =
app.app.installedVersion == app.app.latestVersion;
app.app.latestVersion =
app.app.releaseDate!.microsecondsSinceEpoch.toString();
if (isUpdated) {
app.app.installedVersion = app.app.latestVersion;
}
var versionDetectionEnabled =
app.app.additionalSettings['versionDetection'] == true &&
originalSettings['versionDetection'] != true;
var releaseDateVersionEnabled =
app.app.additionalSettings['releaseDateAsVersion'] == true &&
originalSettings['releaseDateAsVersion'] != true;
var releaseDateVersionDisabled =
app.app.additionalSettings['releaseDateAsVersion'] != true &&
originalSettings['releaseDateAsVersion'] == true;
if (releaseDateVersionEnabled) {
if (app.app.releaseDate != null) {
bool isUpdated = app.app.installedVersion == app.app.latestVersion;
app.app.latestVersion =
app.app.releaseDate!.microsecondsSinceEpoch.toString();
if (isUpdated) {
app.app.installedVersion = app.app.latestVersion;
}
}
} else if (originalSettings['versionDetection'] ==
'releaseDateAsVersion') {
} else if (releaseDateVersionDisabled) {
app.app.installedVersion =
app.installedInfo?.versionName ?? app.app.installedVersion;
}
if (versionDetectionEnabled) {
app.app.additionalSettings['versionDetection'] = true;
app.app.additionalSettings['releaseDateAsVersion'] = false;
}
appsProvider.saveApps([app.app]).then((value) {
getUpdate(app.app.id);
getUpdate(app.app.id, resetVersion: versionDetectionEnabled);
});
}
}

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -419,7 +421,7 @@ class AppsPageState extends State<AppsPage> {
}
getVersionText(int appIndex) {
return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}';
return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('pseudoVersion')}' : ''}';
}
getChangesButtonString(int appIndex, bool hasChangeLogFn) {
@ -871,20 +873,44 @@ class AppsPageState extends State<AppsPage> {
onPressed: () {
String urls = '';
for (var a in selectedApps) {
urls += 'obtainium://add/${a.url}\n';
urls += '${a.url}\n';
}
urls = urls.substring(0, urls.length - 1);
Share.share(urls,
subject: tr('selectedAppURLsFromObtainium'));
subject:
'${tr('obtainium')} - ${tr('appsString')}');
Navigator.of(context).pop();
},
tooltip: tr('shareSelectedAppURLs'),
icon: const Icon(Icons.share),
icon: const Icon(Icons.share_rounded),
),
IconButton(
onPressed: resetSelectedAppsInstallStatuses,
tooltip: tr('resetInstallStatus'),
icon: const Icon(Icons.restore_page_outlined),
onPressed: selectedAppIds.isEmpty
? null
: () {
String urls =
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
for (var a in selectedApps) {
urls +=
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
'id': a.id,
'url': a.url,
'author': a.author,
'name': a.name,
'preferredApkIndex':
a.preferredApkIndex,
'additionalSettings':
jsonEncode(a.additionalSettings)
}))}">${a.name}</a></li>\n';
}
urls +=
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
Share.share(urls,
subject:
'${tr('obtainium')} - ${tr('appsString')}');
},
tooltip: tr('shareAppConfigLinks'),
icon: const Icon(Icons.ios_share),
),
]),
),

View File

@ -199,10 +199,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
...source.searchQuerySettingFormItems.map((e) => [e]),
[
GeneratedFormTextField('url',
label: source.host != null
label: source.hosts.isNotEmpty
? tr('overrideSource')
: plural('url', 1).substring(2),
defaultValue: source.host ?? '',
defaultValue:
source.hosts.isNotEmpty ? source.hosts[0] : '',
required: true)
],
],
@ -212,7 +213,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
setState(() {
importInProgress = true;
});
if (values['url'] != source.host) {
if (values['url'] != source.hosts[0]) {
source = sourceProvider.getSource(values['url'],
overrideSource: source.runtimeType.toString());
}

View File

@ -416,13 +416,17 @@ class _SettingsPageState extends State<SettingsPage> {
value: settingsProvider.useSystemFont,
onChanged: (useSystemFont) {
if (useSystemFont) {
NativeFeatures.loadSystemFont().then((fontLoadRes) {
NativeFeatures.loadSystemFont()
.then((fontLoadRes) {
if (fontLoadRes == 'ok') {
settingsProvider.useSystemFont = true;
settingsProvider.useSystemFont =
true;
} else {
showError(ObtainiumError(
tr('systemFontError', args: [fontLoadRes])
), context);
showError(
ObtainiumError(tr(
'systemFontError',
args: [fontLoadRes])),
context);
}
});
} else {
@ -628,38 +632,9 @@ class _SettingsPageState extends State<SettingsPage> {
label: Text(tr('appLogs'))),
],
),
const Divider(
height: 32,
),
// Padding(
// padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
// child: Column(children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Flexible(child: Text(tr('debugMenu'))),
// Switch(
// value: settingsProvider.showDebugOpts,
// onChanged: (value) {
// settingsProvider.showDebugOpts = value;
// })
// ],
// ),
// if (settingsProvider.showDebugOpts)
// Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// height16,
// TextButton(
// onPressed: () {
// bgUpdateCheck('taskId', null);
// showMessage(tr('bgTaskStarted'), context);
// },
// child: Text(tr('runBgCheckNow')))
// ],
// ),
// ]),
// ),
const SizedBox(
height: 16,
)
],
),
)

View File

@ -167,8 +167,24 @@ String hashListOfLists(List<List<int>> data) {
return hash.hashCode.toString();
}
Future<String> checkDownloadHash(String url,
{int bytesToGrab = 1024, Map<String, String>? headers}) async {
Future<String> checkPartialDownloadHashDynamc(String url,
{int startingSize = 1024,
int lowerLimit = 128,
Map<String, String>? headers}) async {
for (int i = startingSize; i >= lowerLimit; i -= 256) {
List<String> ab = await Future.wait([
checkPartialDownloadHash(url, i, headers: headers),
checkPartialDownloadHash(url, i, headers: headers)
]);
if (ab[0] == ab[1]) {
return ab[0];
}
}
throw NoVersionError();
}
Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
{Map<String, String>? headers}) async {
var req = Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
@ -234,6 +250,20 @@ Future<File> downloadFile(
return downloadedFile;
}
Future<PackageInfo?> getInstalledInfo(String? packageName,
{bool printErr = true}) async {
if (packageName != null) {
try {
return await pm.getPackageInfo(packageName: packageName);
} catch (e) {
if (printErr) {
print(e); // OK
}
}
}
return null;
}
class AppsProvider with ChangeNotifier {
// In memory App state (should always be kept in sync with local storage versions)
Map<String, AppInMemory> apps = {};
@ -326,13 +356,15 @@ class AppsProvider with ChangeNotifier {
AppSource source = SourceProvider()
.getSource(app.url, overrideSource: app.overrideSource);
String downloadUrl = await source.apkUrlPrefetchModifier(
app.apkUrls[app.preferredApkIndex].value, app.url);
app.apkUrls[app.preferredApkIndex].value,
app.url,
app.additionalSettings);
var notif = DownloadNotification(app.finalName, 100);
notificationsProvider?.cancel(notif.id);
int? prevProg;
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
var headers = await source.getRequestHeaders(
additionalSettings: app.additionalSettings, forAPKDownload: true);
var headers = await source.getRequestHeaders(app.additionalSettings,
forAPKDownload: true);
var downloadedFile = await downloadFileWithRetry(
downloadUrl, fileNameNoExt,
headers: headers, (double? progress) {
@ -637,6 +669,7 @@ class AppsProvider with ChangeNotifier {
MapEntry<String, String>? apkUrl;
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
if (!trackOnly) {
// ignore: use_build_context_synchronously
apkUrl = await confirmApkUrl(apps[id]!.app, context);
}
if (apkUrl != null) {
@ -773,20 +806,6 @@ class AppsProvider with ChangeNotifier {
return appsDir;
}
Future<PackageInfo?> getInstalledInfo(String? packageName,
{bool printErr = true}) async {
if (packageName != null) {
try {
return await pm.getPackageInfo(packageName: packageName);
} catch (e) {
if (printErr) {
print(e); // OK
}
}
}
return null;
}
bool isVersionDetectionPossible(AppInMemory? app) {
if (app?.app == null) {
return false;
@ -796,13 +815,16 @@ class AppsProvider with ChangeNotifier {
SourceProvider()
.getSource(app.app.url, overrideSource: app.app.overrideSource)
.naiveStandardVersionDetection;
String? realInstalledVersion =
app.app.additionalSettings['useVersionCodeAsOSVersion'] == true
? app.installedInfo?.versionCode.toString()
: app.installedInfo?.versionName;
return app.app.additionalSettings['trackOnly'] != true &&
app.app.additionalSettings['versionDetection'] !=
'releaseDateAsVersion' &&
app.installedInfo?.versionName != null &&
app.app.additionalSettings['releaseDateAsVersion'] != true &&
realInstalledVersion != null &&
app.app.installedVersion != null &&
(reconcileVersionDifferences(app.installedInfo!.versionName!,
app.app.installedVersion!) !=
(reconcileVersionDifferences(
realInstalledVersion, app.app.installedVersion!) !=
null ||
naiveStandardVersionDetection);
}
@ -814,37 +836,39 @@ class AppsProvider with ChangeNotifier {
var modded = false;
var trackOnly = app.additionalSettings['trackOnly'] == true;
var versionDetectionIsStandard =
app.additionalSettings['versionDetection'] ==
'standardVersionDetection';
app.additionalSettings['versionDetection'] == true;
var naiveStandardVersionDetection =
app.additionalSettings['naiveStandardVersionDetection'] == true ||
SourceProvider()
.getSource(app.url, overrideSource: app.overrideSource)
.naiveStandardVersionDetection;
String? realInstalledVersion =
app.additionalSettings['useVersionCodeAsOSVersion'] == true
? installedInfo?.versionCode.toString()
: installedInfo?.versionName;
// FIRST, COMPARE THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE ONE IS NULL
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
// App says it's installed but isn't really (and isn't track only) - set to not installed
app.installedVersion = null;
modded = true;
} else if (installedInfo?.versionName != null &&
app.installedVersion == null) {
// App says it's not installed but really is - set to installed and use real package versionName
app.installedVersion = installedInfo!.versionName;
} else if (realInstalledVersion != null && app.installedVersion == null) {
// App says it's not installed but really is - set to installed and use real package versionName (or versionCode if chosen)
app.installedVersion = realInstalledVersion;
modded = true;
}
// SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL
if (installedInfo?.versionName != null &&
installedInfo!.versionName != app.installedVersion &&
if (realInstalledVersion != null &&
realInstalledVersion != app.installedVersion &&
versionDetectionIsStandard) {
// App's reported version and real version don't match (and it uses standard version detection)
// If they share a standard format (and are still different under it), update the reported version accordingly
var correctedInstalledVersion = reconcileVersionDifferences(
installedInfo.versionName!, app.installedVersion!);
realInstalledVersion, app.installedVersion!);
if (correctedInstalledVersion?.key == false) {
app.installedVersion = correctedInstalledVersion!.value;
modded = true;
} else if (naiveStandardVersionDetection) {
app.installedVersion = installedInfo.versionName;
app.installedVersion = realInstalledVersion;
modded = true;
}
}
@ -866,7 +890,7 @@ class AppsProvider with ChangeNotifier {
versionDetectionIsStandard &&
!isVersionDetectionPossible(
AppInMemory(app, null, installedInfo, null))) {
app.additionalSettings['versionDetection'] = 'noVersionDetection';
app.additionalSettings['versionDetection'] = false;
logs.add('Could not reconcile version formats for: ${app.id}');
modded = true;
}
@ -908,6 +932,17 @@ class AppsProvider with ChangeNotifier {
: false;
}
Future<void> updateInstallStatusInMemory(AppInMemory app) async {
apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id);
apps[app.app.id]?.icon =
await apps[app.app.id]?.installedInfo?.applicationInfo?.getAppIcon();
apps[app.app.id]?.app.name = await (apps[app.app.id]
?.installedInfo
?.applicationInfo
?.getAppLabel()) ??
app.name;
}
Future<void> loadApps({String? singleId}) async {
while (loadingApps) {
await Future.delayed(const Duration(microseconds: 1));
@ -956,19 +991,11 @@ class AppsProvider with ChangeNotifier {
NotificationsProvider().notify(
AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()));
}
for (var app in apps.values) {
// Get install status and other OS info for each App (slow)
apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id);
apps[app.app.id]?.icon =
await apps[app.app.id]?.installedInfo?.applicationInfo?.getAppIcon();
apps[app.app.id]?.app.name = await (apps[app.app.id]
?.installedInfo
?.applicationInfo
?.getAppLabel()) ??
app.name;
notifyListeners();
}
// Get install status and other OS info for each App (slow)
await Future.wait(apps.values.map((app) {
return updateInstallStatusInMemory(app);
}));
notifyListeners();
// Reconcile version differences
List<App> modifiedApps = [];
for (var app in apps.values) {
@ -991,7 +1018,6 @@ class AppsProvider with ChangeNotifier {
}
}
}
loadingApps = false;
notifyListeners();
}
@ -1289,8 +1315,11 @@ class AppsProvider with ChangeNotifier {
await Future.delayed(const Duration(microseconds: 1));
}
for (App a in importedApps) {
var installedInfo = await getInstalledInfo(a.id, printErr: false);
a.installedVersion =
(await getInstalledInfo(a.id, printErr: false))?.versionName;
a.additionalSettings['useVersionCodeAsOSVersion'] == true
? installedInfo?.versionCode.toString()
: installedInfo?.versionName;
}
await saveApps(importedApps, onlyIfExists: false);
notifyListeners();

View File

@ -14,8 +14,9 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
String obtainiumTempId = 'imranr98_obtainium_${GitHub().hosts[0]}';
String obtainiumId = 'dev.imranr.obtainium';
String obtainiumUrl = 'https://github.com/ImranR98/Obtainium';
enum InstallMethodSettings { normal, shizuku, root }

View File

@ -11,6 +11,7 @@ import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/apkpure.dart';
import 'package:obtainium/app_sources/aptoide.dart';
import 'package:obtainium/app_sources/codeberg.dart';
import 'package:obtainium/app_sources/directAPKLink.dart';
import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/fdroidrepo.dart';
import 'package:obtainium/app_sources/github.dart';
@ -19,7 +20,6 @@ import 'package:obtainium/app_sources/huaweiappgallery.dart';
import 'package:obtainium/app_sources/izzyondroid.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/app_sources/jenkins.dart';
import 'package:obtainium/app_sources/mullvad.dart';
import 'package:obtainium/app_sources/neutroncode.dart';
import 'package:obtainium/app_sources/signal.dart';
import 'package:obtainium/app_sources/sourceforge.dart';
@ -104,6 +104,21 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
additionalSettings.remove('releaseDateAsVersion');
}
}
// Convert dropdown style version detection options back into bool style
if (additionalSettings['versionDetection'] == 'standardVersionDetection') {
additionalSettings['versionDetection'] = true;
} else if (additionalSettings['versionDetection'] == 'noVersionDetection') {
additionalSettings['versionDetection'] = false;
} else if (additionalSettings['versionDetection'] == 'releaseDateAsVersion') {
additionalSettings['versionDetection'] = false;
additionalSettings['releaseDateAsVersion'] = true;
}
// Convert bool style pseudo version method to dropdown style
if (originalAdditionalSettings['supportFixedAPKURL'] == true) {
additionalSettings['defaultPseudoVersioningMethod'] = 'partialAPKHash';
} else if (originalAdditionalSettings['supportFixedAPKURL'] == false) {
additionalSettings['defaultPseudoVersioningMethod'] = 'APKLinkHash';
}
// Ensure additionalSettings are correctly typed
for (var item in formItems) {
if (additionalSettings[item.key] != null) {
@ -277,9 +292,10 @@ class App {
json['installedVersion'] == null
? null
: json['installedVersion'] as String,
json['latestVersion'] as String,
assumed2DlistToStringMapList(jsonDecode(json['apkUrls'])),
json['preferredApkIndex'] as int,
(json['latestVersion'] ?? tr('unknown')) as String,
assumed2DlistToStringMapList(jsonDecode(
(json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
(json['preferredApkIndex'] ?? -1) as int,
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
json['lastUpdateCheck'] == null
? null
@ -366,8 +382,12 @@ List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
}).toList();
getSourceRegex(List<String> hosts) {
return '(${hosts.join('|').replaceAll('.', '\\.')})';
}
abstract class AppSource {
String? host;
List<String> hosts = [];
bool hostChanged = false;
late String name;
bool enforceTrackOnly = false;
@ -376,28 +396,24 @@ abstract class AppSource {
bool allowSubDomains = false;
bool naiveStandardVersionDetection = false;
bool neverAutoSelect = false;
bool showReleaseDateAsVersionToggle = false;
bool versionDetectionDisallowed = false;
List<String> excludeCommonSettingKeys = [];
AppSource() {
name = runtimeType.toString();
}
overrideVersionDetectionFormDefault(String vd,
{bool disableStandard = false, bool disableRelDate = false}) {
additionalAppSpecificSourceAgnosticSettingFormItems =
additionalAppSpecificSourceAgnosticSettingFormItems.map((e) {
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(String key,
{bool disabled = true, bool defaultValue = true}) {
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
.map((e) {
return e.map((e2) {
if (e2.key == 'versionDetection') {
var item = e2 as GeneratedFormDropdown;
item.defaultValue = vd;
item.disabledOptKeys = [];
if (disableStandard) {
item.disabledOptKeys?.add('standardVersionDetection');
}
if (disableRelDate) {
item.disabledOptKeys?.add('releaseDateAsVersion');
}
item.disabledOptKeys =
item.disabledOptKeys?.where((element) => element != vd).toList();
if (e2.key == key) {
var item = e2 as GeneratedFormSwitch;
item.disabled = disabled;
item.defaultValue = defaultValue;
}
return e2;
}).toList();
@ -413,8 +429,8 @@ abstract class AppSource {
}
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
return null;
}
@ -422,12 +438,10 @@ abstract class AppSource {
return app;
}
Future<Response> sourceRequest(String url,
{bool followRedirects = true,
Map<String, dynamic> additionalSettings =
const <String, dynamic>{}}) async {
var requestHeaders =
await getRequestHeaders(additionalSettings: additionalSettings);
Future<Response> sourceRequest(
String url, Map<String, dynamic> additionalSettings,
{bool followRedirects = true}) async {
var requestHeaders = await getRequestHeaders(additionalSettings);
if (requestHeaders != null || followRedirects == false) {
var req = Request('GET', Uri.parse(url));
req.followRedirects = followRedirects;
@ -455,7 +469,7 @@ abstract class AppSource {
// Some additional data may be needed for Apps regardless of Source
List<List<GeneratedFormItem>>
additionalAppSpecificSourceAgnosticSettingFormItems = [
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = [
[
GeneratedFormSwitch(
'trackOnly',
@ -473,16 +487,12 @@ abstract class AppSource {
label: tr('matchGroupToUse'), required: false, hint: '\$0')
],
[
GeneratedFormDropdown(
'versionDetection',
[
MapEntry(
'standardVersionDetection', tr('standardVersionDetection')),
MapEntry('releaseDateAsVersion', tr('releaseDateAsVersion')),
MapEntry('noVersionDetection', tr('noVersionDetection'))
],
label: tr('versionDetection'),
defaultValue: 'standardVersionDetection')
GeneratedFormSwitch('versionDetection',
label: tr('versionDetectionExplanation'), defaultValue: true)
],
[
GeneratedFormSwitch('useVersionCodeAsOSVersion',
label: tr('useVersionCodeAsOSVersion'), defaultValue: false)
],
[
GeneratedFormTextField('apkFilterRegEx',
@ -494,6 +504,11 @@ abstract class AppSource {
}
])
],
[
GeneratedFormSwitch('invertAPKFilter',
label: '${tr('invertRegEx')} (${tr('filterAPKsByRegEx')})',
defaultValue: false)
],
[
GeneratedFormSwitch('autoApkFilterByArch',
label: tr('autoApkFilterByArch'), defaultValue: true)
@ -512,9 +527,48 @@ abstract class AppSource {
// Previous 2 variables combined into one at runtime for convenient usage
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
if (showReleaseDateAsVersionToggle == true) {
if (additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
.indexWhere((List<GeneratedFormItem> e) =>
e.indexWhere((GeneratedFormItem i) =>
i.key == 'releaseDateAsVersion') >=
0) <
0) {
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly.insert(
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
.indexWhere((List<GeneratedFormItem> e) =>
e.indexWhere((GeneratedFormItem i) =>
i.key == 'versionDetection') >=
0) +
1,
[
GeneratedFormSwitch('releaseDateAsVersion',
label:
'${tr('releaseDateAsVersion')} (${tr('pseudoVersion')})',
defaultValue: false)
]);
}
}
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
.map((e) => e
.where((ee) => !excludeCommonSettingKeys.contains(ee.key))
.toList())
.where((e) => e.isNotEmpty)
.toList();
if (versionDetectionDisallowed) {
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
'versionDetection',
disabled: true,
defaultValue: false);
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
'useVersionCodeAsOSVersion',
disabled: true,
defaultValue: false);
}
return [
...additionalSourceAppSpecificSettingFormItems,
...additionalAppSpecificSourceAgnosticSettingFormItems
...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
];
}
@ -544,8 +598,8 @@ abstract class AppSource {
return null;
}
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
Map<String, dynamic> additionalSettings) async {
return apkUrl;
}
@ -659,6 +713,20 @@ String? extractVersion(String? versionExtractionRegEx, String? matchGroupString,
}
}
List<MapEntry<String, String>> filterApks(
List<MapEntry<String, String>> apkUrls,
String? apkFilterRegEx,
bool? invert) {
if (apkFilterRegEx?.isNotEmpty == true) {
var reg = RegExp(apkFilterRegEx!);
apkUrls = apkUrls.where((element) {
var hasMatch = reg.hasMatch(element.key);
return invert == true ? !hasMatch : hasMatch;
}).toList();
}
return apkUrls;
}
class SourceProvider {
// Add more source classes here so they are available via the service
List<AppSource> get sources => [
@ -676,12 +744,12 @@ class SourceProvider {
APKMirror(),
HuaweiAppGallery(),
Jenkins(),
Mullvad(),
Signal(),
VLC(),
WhatsApp(),
TelegramApp(),
NeutronCode(),
DirectAPKLink(),
HTML() // This should ALWAYS be the last option as they are tried in order
];
@ -697,22 +765,26 @@ class SourceProvider {
throw UnsupportedURLError();
}
var res = srcs.first;
res.host = Uri.parse(url).host;
res.hosts = [Uri.parse(url).host];
res.hostChanged = true;
return srcs.first;
}
AppSource? source;
for (var s in sources.where((element) => element.host != null)) {
if (RegExp(
'://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?')
.hasMatch(url)) {
source = s;
break;
for (var s in sources.where((element) => element.hosts.isNotEmpty)) {
try {
if (RegExp(
'^${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})\$')
.hasMatch(Uri.parse(url).host)) {
source = s;
break;
}
} catch (e) {
// Ignore
}
}
if (source == null) {
for (var s in sources.where(
(element) => element.host == null && !element.neverAutoSelect)) {
(element) => element.hosts.isEmpty && !element.neverAutoSelect)) {
try {
s.sourceSpecificStandardizeURL(url);
source = s;
@ -768,15 +840,12 @@ class SourceProvider {
}
}
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
if (additionalSettings['releaseDateAsVersion'] == true &&
apk.releaseDate != null) {
apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
}
if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
apk.apkUrls =
apk.apkUrls.where((element) => reg.hasMatch(element.key)).toList();
}
apk.apkUrls = filterApks(apk.apkUrls, additionalSettings['apkFilterRegEx'],
additionalSettings['invertAPKFilter']);
if (apk.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError();
}

View File

@ -22,10 +22,10 @@ packages:
dependency: "direct main"
description:
name: android_package_manager
sha256: b873fe5856f7c442aca9751dac05d117285be9e4de08eb15d1ffb811fd1b688d
sha256: e52ca607b9f19f95d5dae4211ed8fa93e67093f22ac570db47489c5bca512940
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.7.0"
animations:
dependency: "direct main"
description:
@ -70,10 +70,10 @@ packages:
dependency: "direct main"
description:
name: background_fetch
sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4
sha256: "34550cf9b383e5a1844e7d22119aa500508c7df9421fa967c9fb4430d6cb2878"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
boolean_selector:
dependency: transitive
description:
@ -258,6 +258,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -299,10 +307,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535"
sha256: "66cc2fe16bf4bca71d795939763ad3f1830ad85772dc3b1561613c501859826d"
url: "https://pub.dev"
source: hosted
version: "16.3.0"
version: "16.3.1+1"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -386,10 +394,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@ -402,10 +410,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d"
url: "https://pub.dev"
source: hosted
version: "4.1.3"
version: "4.1.4"
intl:
dependency: transitive
description:
@ -442,10 +450,10 @@ packages:
dependency: transitive
description:
name: markdown
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
version: "7.2.1"
matcher:
dependency: transitive
description:
@ -506,10 +514,10 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
path_provider_android:
dependency: transitive
description:
@ -522,10 +530,10 @@ packages:
dependency: transitive
description:
name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
path_provider_linux:
dependency: transitive
description:
@ -538,10 +546,10 @@ packages:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
@ -554,50 +562,50 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78"
sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6"
url: "https://pub.dev"
source: hosted
version: "11.1.0"
version: "11.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6"
sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb"
url: "https://pub.dev"
source: hosted
version: "12.0.1"
version: "12.0.3"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306"
sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830
url: "https://pub.dev"
source: hosted
version: "9.2.0"
version: "9.3.0"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df"
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
version: "0.1.1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1
sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004"
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
petitparser:
dependency: transitive
description:
@ -626,10 +634,10 @@ packages:
dependency: transitive
description:
name: pointycastle
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.3"
version: "3.7.4"
provider:
dependency: "direct main"
description:
@ -674,10 +682,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
@ -690,10 +698,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
@ -823,26 +831,26 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96
url: "https://pub.dev"
source: hosted
version: "6.2.2"
version: "6.2.3"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51
sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
url: "https://pub.dev"
source: hosted
version: "6.2.1"
version: "6.2.2"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea"
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
url: "https://pub.dev"
source: hosted
version: "6.2.2"
version: "6.2.4"
url_launcher_linux:
dependency: transitive
description:
@ -863,10 +871,10 @@ packages:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198"
sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.1"
url_launcher_web:
dependency: transitive
description:
@ -887,10 +895,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.3.3"
vector_math:
dependency: transitive
description:
@ -911,10 +919,10 @@ packages:
dependency: "direct main"
description:
name: webview_flutter
sha256: "60e23976834e995c404c0b21d3b9db37ecd77d3303ef74f8b8d7a7b19947fc04"
sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c"
url: "https://pub.dev"
source: hosted
version: "4.4.3"
version: "4.4.4"
webview_flutter_android:
dependency: transitive
description:
@ -927,18 +935,18 @@ packages:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
sha256: "80b40ae4fb959957eef9fa8970b6c9accda9f49fc45c2b75154696a8e8996cfe"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
version: "2.9.1"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "02d8f3ebbc842704b2b662377b3ee11c0f8f1bbaa8eab6398262f40049819160"
sha256: b99ca8d8bae9c6b43d568218691aa537fb0aeae1d7d34eadf112a6aa36d26506
url: "https://pub.dev"
source: hosted
version: "3.10.1"
version: "3.11.0"
win32:
dependency: transitive
description:
@ -980,5 +988,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"
dart: ">=3.2.3 <4.0.0"
flutter: ">=3.16.6"

View File

@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.15.7+243 # When changing this, update the tag in main() accordingly
version: 0.16.0+248 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@ -55,7 +55,7 @@ dependencies:
git:
url: https://github.com/ImranR98/android_package_installer
ref: main
android_package_manager: ^0.6.0
android_package_manager: ^0.7.0
share_plus: ^7.0.0
sqflite: ^2.2.0+3
easy_localization: ^3.0.1