mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-25 02:19:41 +02:00
Compare commits
38 Commits
v0.15.6-be
...
v0.15.10-b
Author | SHA1 | Date | |
---|---|---|---|
|
d44139dd72 | ||
|
60869a0490 | ||
|
102be5fa2e | ||
|
a25c04b390 | ||
|
d46f0a1c33 | ||
|
5c7f5a99e1 | ||
|
d949a80f19 | ||
|
6b5d4e2027 | ||
|
199d071ee8 | ||
|
db4f13e2e1 | ||
|
5e869b7e03 | ||
|
34fee36132 | ||
|
ea09bc36a5 | ||
|
f7e0678cb2 | ||
|
223ae378a9 | ||
|
6f52a48991 | ||
|
0280935955 | ||
|
eebc7d9ab3 | ||
|
aa7b5652ff | ||
|
ec6683a198 | ||
|
05372924a0 | ||
|
e63c1399dd | ||
|
4fcad92e1d | ||
|
e18e7298bc | ||
|
76a6a509cd | ||
|
7a03561ff6 | ||
|
70e54ce14a | ||
|
b5f86f0e79 | ||
|
4ebca49ef7 | ||
|
49cfa95463 | ||
|
4600ab0593 | ||
|
7f2ca98bde | ||
|
6511485bcf | ||
|
fd22113e44 | ||
|
e8580dc1d5 | ||
|
daffff7eb0 | ||
|
751fda5e37 | ||
|
1e38abc500 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
for apk in ./build/app/outputs/flutter-apk/*-release*.apk; do
|
for apk in ./build/app/outputs/flutter-apk/*-release*.apk; do
|
||||||
unsignedFn=${apk/-release/-unsigned}
|
unsignedFn=${apk/-release/-unsigned}
|
||||||
mv "$apk" "$unsignedFn"
|
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
|
sha256sum ${apk} | cut -d " " -f 1 > "$apk".sha256
|
||||||
gpg --batch --pinentry-mode loopback --passphrase "${PGP_PASSPHRASE}" --sign --detach-sig "$apk".sha256
|
gpg --batch --pinentry-mode loopback --passphrase "${PGP_PASSPHRASE}" --sign --detach-sig "$apk".sha256
|
||||||
done
|
done
|
||||||
|
@@ -21,7 +21,7 @@ Currently supported App sources:
|
|||||||
- [SourceForge](https://sourceforge.net/)
|
- [SourceForge](https://sourceforge.net/)
|
||||||
- [SourceHut](https://git.sr.ht/)
|
- [SourceHut](https://git.sr.ht/)
|
||||||
- Other - General:
|
- Other - General:
|
||||||
- [APKPure](https://apkpure.com/)
|
- [APKPure](https://apkpure.net/)
|
||||||
- [Aptoide](https://aptoide.com/)
|
- [Aptoide](https://aptoide.com/)
|
||||||
- [Uptodown](https://uptodown.com/)
|
- [Uptodown](https://uptodown.com/)
|
||||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Otkvači sa vrha",
|
"unpinFromTop": "Otkvači sa vrha",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Resetujte status instalacije za odabrane aplikacije?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Podijeli odabrane URL-ove aplikacija",
|
||||||
"resetInstallStatus": "Resetujte status instalacije",
|
"resetInstallStatus": "Resetujte status instalacije",
|
||||||
"more": "Više",
|
"more": "Više",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Želite li ukloniti aplikaciju?",
|
"one": "Želite li ukloniti aplikaciju?",
|
||||||
"other": "Želite li ukloniti aplikacije?"
|
"other": "Želite li ukloniti aplikacije?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Odepnout shora",
|
"unpinFromTop": "Odepnout shora",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
|
"resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
|
||||||
"installStatusOfXWillBeResetExplanation": "Stav instalace vybraných aplikací bude resetován. To může být užitečné, pokud je verze aplikace zobrazená v Obtainium nesprávná z důvodu neúspěšných aktualizací nebo jiných problémů.",
|
"installStatusOfXWillBeResetExplanation": "Stav instalace vybraných aplikací bude resetován. To může být užitečné, pokud je verze aplikace zobrazená v Obtainium nesprávná z důvodu neúspěšných aktualizací nebo jiných problémů.",
|
||||||
|
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
|
"shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
|
||||||
"resetInstallStatus": "Obnovit stav instalace",
|
"resetInstallStatus": "Obnovit stav instalace",
|
||||||
"more": "Více",
|
"more": "Více",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Správce",
|
"root": "Správce",
|
||||||
"shizukuBinderNotFound": "Shizuku neběží",
|
"shizukuBinderNotFound": "Shizuku neběží",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Odstranit Apku?",
|
"one": "Odstranit Apku?",
|
||||||
"other": "Odstranit Apky?"
|
"other": "Odstranit Apky?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "„Oben anheften“ aufheben",
|
"unpinFromTop": "„Oben anheften“ aufheben",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
|
"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.",
|
"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": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
|
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
|
||||||
"resetInstallStatus": "Installationsstatus zurücksetzen",
|
"resetInstallStatus": "Installationsstatus zurücksetzen",
|
||||||
"more": "Mehr",
|
"more": "Mehr",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku läuft nicht",
|
"shizukuBinderNotFound": "Shizuku läuft nicht",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App entfernen?",
|
"one": "App entfernen?",
|
||||||
"other": "Apps entfernen?"
|
"other": "Apps entfernen?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Unpin from top",
|
"unpinFromTop": "Unpin from top",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Reset Install Status for Selected Apps?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Share Selected App URLs",
|
||||||
"resetInstallStatus": "Reset Install Status",
|
"resetInstallStatus": "Reset Install Status",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
@@ -289,6 +291,8 @@
|
|||||||
"shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
|
"shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
|
||||||
"useSystemFont": "Use the system font",
|
"useSystemFont": "Use the system font",
|
||||||
"systemFontError": "Error loading the system font: {}",
|
"systemFontError": "Error loading the system font: {}",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove App?",
|
"one": "Remove App?",
|
||||||
"other": "Remove Apps?"
|
"other": "Remove Apps?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Desfijar de arriba",
|
"unpinFromTop": "Desfijar de arriba",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar estado de instalación para las aplicaciones seleccionadas?",
|
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar estado de instalación para las aplicaciones seleccionadas?",
|
||||||
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
||||||
|
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "Compartir URLs de las aplicaciones seleccionadas",
|
"shareSelectedAppURLs": "Compartir URLs de las aplicaciones seleccionadas",
|
||||||
"resetInstallStatus": "Restaurar Estado de Instalación",
|
"resetInstallStatus": "Restaurar Estado de Instalación",
|
||||||
"more": "Más",
|
"more": "Más",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku no está operativo",
|
"shizukuBinderNotFound": "Shizuku no está operativo",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "¿Eliminar Aplicación?",
|
"one": "¿Eliminar Aplicación?",
|
||||||
"other": "¿Eliminar Aplicaciones?"
|
"other": "¿Eliminar Aplicaciones?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "برداشتن پین از بالا",
|
"unpinFromTop": "برداشتن پین از بالا",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "وضعیت نصب برنامههای انتخابی بازنشانی شود؟",
|
"resetInstallStatusForSelectedAppsQuestion": "وضعیت نصب برنامههای انتخابی بازنشانی شود؟",
|
||||||
"installStatusOfXWillBeResetExplanation": "وضعیت نصب برنامههای انتخابشده بازنشانی میشود.\n\nاگر نسخه برنامه نشاندادهشده در Obtainium به دلیل بهروزرسانیهای ناموفق یا مشکلات دیگر نادرست باشد، میتواند کمک کند.",
|
"installStatusOfXWillBeResetExplanation": "وضعیت نصب برنامههای انتخابشده بازنشانی میشود.\n\nاگر نسخه برنامه نشاندادهشده در Obtainium به دلیل بهروزرسانیهای ناموفق یا مشکلات دیگر نادرست باشد، میتواند کمک کند.",
|
||||||
|
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "اشتراک گذاری آدرس اینترنتی برنامه های انتخاب شده",
|
"shareSelectedAppURLs": "اشتراک گذاری آدرس اینترنتی برنامه های انتخاب شده",
|
||||||
"resetInstallStatus": "بازنشانی وضعیت نصب",
|
"resetInstallStatus": "بازنشانی وضعیت نصب",
|
||||||
"more": "بیشتر",
|
"more": "بیشتر",
|
||||||
@@ -85,22 +87,22 @@
|
|||||||
"author": "سازنده",
|
"author": "سازنده",
|
||||||
"upToDateApps": "برنامه های به روز",
|
"upToDateApps": "برنامه های به روز",
|
||||||
"nonInstalledApps": "برنامه های نصب نشده",
|
"nonInstalledApps": "برنامه های نصب نشده",
|
||||||
"importExport": "وادر کردن/صادر کردن",
|
"importExport": "درون ریزی/برون ریزی",
|
||||||
"settings": "تنظیمات",
|
"settings": "تنظیمات",
|
||||||
"exportedTo": "صادر کردن به{}",
|
"exportedTo": "برون ریزی به{}",
|
||||||
"obtainiumExport": "صادرکردن Obtainium",
|
"obtainiumExport": "صادرکردن Obtainium",
|
||||||
"invalidInput": "ورودی نامعتبر",
|
"invalidInput": "ورودی نامعتبر",
|
||||||
"importedX": "وارد شده {}",
|
"importedX": "وارد شده {}",
|
||||||
"obtainiumImport": "واردکردن Obtainium",
|
"obtainiumImport": "واردکردن Obtainium",
|
||||||
"importFromURLList": "وارد کردن از فهرست آدرس اینترنتی",
|
"importFromURLList": "درون ریزی از فهرست آدرس اینترنتی",
|
||||||
"searchQuery": "جستجوی سوال",
|
"searchQuery": "جستجوی سوال",
|
||||||
"appURLList": "فهرست آدرس اینترنتی برنامه",
|
"appURLList": "فهرست آدرس اینترنتی برنامه",
|
||||||
"line": "خط",
|
"line": "خط",
|
||||||
"searchX": "جستجو {}",
|
"searchX": "جستجو {}",
|
||||||
"noResults": "نتیجه ای پیدا نشد",
|
"noResults": "نتیجه ای پیدا نشد",
|
||||||
"importX": "وارد کردن {}",
|
"importX": "درون ریزی {}",
|
||||||
"importedAppsIdDisclaimer": "ممکن است برنامههای وارد شده به اشتباه بهعنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی دادههای برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روشهای وارد کردن شخص ثالث تأثیر میگذارد.",
|
"importedAppsIdDisclaimer": "ممکن است برنامههای وارد شده به اشتباه بهعنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی دادههای برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روشهای درون ریزی شخص ثالث تأثیر میگذارد.",
|
||||||
"importErrors": "خطاهای وارد کردن",
|
"importErrors": "خطاهای درون ریزی",
|
||||||
"importedXOfYApps": "{} از {} برنامه وارد شد.",
|
"importedXOfYApps": "{} از {} برنامه وارد شد.",
|
||||||
"followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
|
"followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
|
||||||
"selectURL": "آدرس اینترنتی انتخاب شده",
|
"selectURL": "آدرس اینترنتی انتخاب شده",
|
||||||
@@ -133,7 +135,7 @@
|
|||||||
"close": "بستن",
|
"close": "بستن",
|
||||||
"share": "اشتراک گذاری",
|
"share": "اشتراک گذاری",
|
||||||
"appNotFound": "برنامه پیدا نشد",
|
"appNotFound": "برنامه پیدا نشد",
|
||||||
"obtainiumExportHyphenatedLowercase": "صادر کردن-obtainium",
|
"obtainiumExportHyphenatedLowercase": "برون ریزی-obtainium",
|
||||||
"pickAnAPK": "یک APK انتخاب کنید",
|
"pickAnAPK": "یک APK انتخاب کنید",
|
||||||
"appHasMoreThanOnePackage": "{} بیش از یک بسته دارد:",
|
"appHasMoreThanOnePackage": "{} بیش از یک بسته دارد:",
|
||||||
"deviceSupportsXArch": "دستگاه شما از معماری پردازنده {} پشتیبانی میکند",
|
"deviceSupportsXArch": "دستگاه شما از معماری پردازنده {} پشتیبانی میکند",
|
||||||
@@ -210,7 +212,7 @@
|
|||||||
"releaseDateAsVersionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند، اما تاریخ انتشار در دسترس است.",
|
"releaseDateAsVersionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند، اما تاریخ انتشار در دسترس است.",
|
||||||
"changes": "تغییرات",
|
"changes": "تغییرات",
|
||||||
"releaseDate": "تاریخ انتشار",
|
"releaseDate": "تاریخ انتشار",
|
||||||
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
"importFromURLsInFile": "درون ریزی از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
||||||
"versionDetection": "تشخیص نسخه",
|
"versionDetection": "تشخیص نسخه",
|
||||||
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
||||||
"groupByCategory": "گروه بر اساس دسته",
|
"groupByCategory": "گروه بر اساس دسته",
|
||||||
@@ -235,7 +237,7 @@
|
|||||||
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
|
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
|
||||||
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
|
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
|
||||||
"gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
|
"gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
|
||||||
"sortByLastLinkSegment": "Sort by only the last segment of the link",
|
"sortByLastLinkSegment": "فقط بر اساس آخرین بخش پیوند مرتب کنید",
|
||||||
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
|
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
|
||||||
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
|
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
|
||||||
"appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
|
"appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
|
||||||
@@ -245,25 +247,25 @@
|
|||||||
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
|
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
|
||||||
"backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
|
"backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
|
||||||
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
|
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
|
||||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
|
"intermediateLinkRegex": "برای بازدید از پیوند «میانگین» فیلتر کنید",
|
||||||
"filterByLinkText": "Filter links by link text",
|
"filterByLinkText": "لینک ها را بر اساس متن پیوند فیلتر کنید",
|
||||||
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
|
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
|
||||||
"intermediateLink": "Intermediate link",
|
"intermediateLink": "پیوند میانی",
|
||||||
"exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
|
"exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
|
||||||
"bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
|
"bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
|
||||||
"autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
|
"autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
|
||||||
"versionExtractionRegEx": "نسخه استخراج RegEx",
|
"versionExtractionRegEx": "نسخه استخراج RegEx",
|
||||||
"matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
|
"matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
|
||||||
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
|
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
|
||||||
"pickExportDir": "فهرست صادرات را انتخاب کنید",
|
"pickExportDir": "فهرست برون ریزی را انتخاب کنید",
|
||||||
"autoExportOnChanges": "صادرات خودکار تغییرات",
|
"autoExportOnChanges": "برون ریزی خودکار تغییرات",
|
||||||
"includeSettings": "Include settings",
|
"includeSettings": "شامل تنظیمات",
|
||||||
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
|
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
|
||||||
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
|
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
|
||||||
"dontSortReleasesList": "حفظ سفارش انتشار از API",
|
"dontSortReleasesList": "حفظ سفارش انتشار از API",
|
||||||
"reverseSort": "مرتب سازی معکوس",
|
"reverseSort": "مرتب سازی معکوس",
|
||||||
"takeFirstLink": "Take first link",
|
"takeFirstLink": "لینک اول را بگیرید",
|
||||||
"skipSort": "Skip sorting",
|
"skipSort": "از مرتب سازی صرف نظر کنید",
|
||||||
"debugMenu": "منوی اشکال زدایی",
|
"debugMenu": "منوی اشکال زدایی",
|
||||||
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
|
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
|
||||||
"runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
|
"runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
|
||||||
@@ -281,12 +283,14 @@
|
|||||||
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
|
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
|
||||||
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
|
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
|
||||||
"selectX": "انتخاب کنید {}",
|
"selectX": "انتخاب کنید {}",
|
||||||
"parallelDownloads": "Allow parallel downloads",
|
"parallelDownloads": "اجازه دانلود موازی",
|
||||||
"installMethod": "Installation method",
|
"installMethod": "روش نصب",
|
||||||
"normal": "Normal",
|
"normal": "Normal",
|
||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku در حال اجرا نیست",
|
||||||
|
"useVersionCodeAsOSVersion": "استفاده کد نسخه برنامه به جای نسخه شناسایی شده توسط سیستم عامل استفاده کنید",
|
||||||
|
"requestHeader": "درخواست سطر بالایی",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "برنامه حذف شود؟",
|
"one": "برنامه حذف شود؟",
|
||||||
"other": "برنامه ها حذف شوند؟"
|
"other": "برنامه ها حذف شوند؟"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Détacher du haut",
|
"unpinFromTop": "Détacher du haut",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Réinitialiser l'état d'installation des applications sélectionnées ?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Partager les URL d'application sélectionnées",
|
||||||
"resetInstallStatus": "Réinitialiser le statut d'installation",
|
"resetInstallStatus": "Réinitialiser le statut d'installation",
|
||||||
"more": "Plus",
|
"more": "Plus",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Supprimer l'application ?",
|
"one": "Supprimer l'application ?",
|
||||||
"other": "Supprimer les applications ?"
|
"other": "Supprimer les applications ?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Eltávolít felülről",
|
"unpinFromTop": "Eltávolít felülről",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Visszaállítja a kiválasztott appok telepítési állapotát?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit",
|
||||||
"resetInstallStatus": "Telepítési állapot visszaállítása",
|
"resetInstallStatus": "Telepítési állapot visszaállítása",
|
||||||
"more": "További",
|
"more": "További",
|
||||||
@@ -244,10 +246,10 @@
|
|||||||
"backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
|
"backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
|
||||||
"backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
|
"backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
|
||||||
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
|
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
|
||||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
|
"intermediateLinkRegex": "Szűrés egy 'köztes' látogatási linkre",
|
||||||
"filterByLinkText": "Filter links by link text",
|
"filterByLinkText": "A hivatkozások szűrése linkszöveg alapján",
|
||||||
"intermediateLinkNotFound": "Közvetítő link nem található",
|
"intermediateLinkNotFound": "Köztes link nem található",
|
||||||
"intermediateLink": "Intermediate link",
|
"intermediateLink": "Köztes link",
|
||||||
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
|
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
|
||||||
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
|
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
|
||||||
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
|
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
|
||||||
@@ -261,8 +263,8 @@
|
|||||||
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
|
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
|
||||||
"dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
|
"dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
|
||||||
"reverseSort": "Fordított rendezés",
|
"reverseSort": "Fordított rendezés",
|
||||||
"takeFirstLink": "Take first link",
|
"takeFirstLink": "Vegye az első linket",
|
||||||
"skipSort": "Skip sorting",
|
"skipSort": "A válogatás kihagyása",
|
||||||
"debugMenu": "Hibakereső menü",
|
"debugMenu": "Hibakereső menü",
|
||||||
"bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
|
"bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
|
||||||
"enableBackgroundUpdates": "Frissítések a háttérben",
|
"enableBackgroundUpdates": "Frissítések a háttérben",
|
||||||
@@ -282,11 +284,13 @@
|
|||||||
"supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
|
"supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
|
||||||
"selectX": "Kiválaszt {}",
|
"selectX": "Kiválaszt {}",
|
||||||
"parallelDownloads": "Párhuzamos letöltéseket enged",
|
"parallelDownloads": "Párhuzamos letöltéseket enged",
|
||||||
"installMethod": "Installation method",
|
"installMethod": "Telepítési mód",
|
||||||
"normal": "Normal",
|
"normal": "Normál",
|
||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"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",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Eltávolítja az alkalmazást?",
|
"one": "Eltávolítja az alkalmazást?",
|
||||||
"other": "Eltávolítja az alkalmazást?"
|
"other": "Eltávolítja az alkalmazást?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Rimuovi dall'alto",
|
"unpinFromTop": "Rimuovi dall'alto",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Ripristinare lo stato d'installazione delle app selezionate?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Condividi gli URL delle app selezionate",
|
||||||
"resetInstallStatus": "Ripristina lo stato d'installazione",
|
"resetInstallStatus": "Ripristina lo stato d'installazione",
|
||||||
"more": "Altro",
|
"more": "Altro",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku non è in esecuzione",
|
"shizukuBinderNotFound": "Shizuku non è in esecuzione",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Rimuovere l'app?",
|
"one": "Rimuovere l'app?",
|
||||||
"other": "Rimuovere le app?"
|
"other": "Rimuovere le app?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "トップから固定解除",
|
"unpinFromTop": "トップから固定解除",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
||||||
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
|
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
|
||||||
|
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
||||||
"resetInstallStatus": "インストール状態をリセットする",
|
"resetInstallStatus": "インストール状態をリセットする",
|
||||||
"more": "もっと見る",
|
"more": "もっと見る",
|
||||||
@@ -287,6 +289,10 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizukuが起動していません",
|
"shizukuBinderNotFound": "Shizukuが起動していません",
|
||||||
|
"useSystemFont": "システムフォントを使用する",
|
||||||
|
"systemFontError": "システムフォントの読み込みエラー: {}",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "アプリを削除しますか?",
|
"one": "アプリを削除しますか?",
|
||||||
"other": "アプリを削除しますか?"
|
"other": "アプリを削除しますか?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Losmaken van de bovenkant",
|
"unpinFromTop": "Losmaken van de bovenkant",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Installatiestatus resetten voor geselecteerde apps?",
|
"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.",
|
"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": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "Deel geselecteerde app URL's",
|
"shareSelectedAppURLs": "Deel geselecteerde app URL's",
|
||||||
"resetInstallStatus": "Reset installatiestatus",
|
"resetInstallStatus": "Reset installatiestatus",
|
||||||
"more": "Meer",
|
"more": "Meer",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App verwijderen?",
|
"one": "App verwijderen?",
|
||||||
"other": "Apps verwijderen?"
|
"other": "Apps verwijderen?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Odepnij",
|
"unpinFromTop": "Odepnij",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Zresetować status instalacji dla wybranych aplikacji?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Udostępnij wybrane adresy URL aplikacji",
|
||||||
"resetInstallStatus": "Zresetuj stan instalacji",
|
"resetInstallStatus": "Zresetuj stan instalacji",
|
||||||
"more": "Więcej",
|
"more": "Więcej",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Usunąć aplikację?",
|
"one": "Usunąć aplikację?",
|
||||||
"few": "Usunąć aplikacje?",
|
"few": "Usunąć aplikacje?",
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Desafixar do topo",
|
"unpinFromTop": "Desafixar do topo",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Reiniciar status de instalação para aplicativos selecionados?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Compartilhar URLs de aplicativos selecionados",
|
||||||
"resetInstallStatus": "Reiniciar status de Iistalação",
|
"resetInstallStatus": "Reiniciar status de Iistalação",
|
||||||
"more": "Mais",
|
"more": "Mais",
|
||||||
@@ -220,7 +222,6 @@
|
|||||||
"dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas Monitorar'",
|
"dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas Monitorar'",
|
||||||
"dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK",
|
"dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK",
|
||||||
"moveNonInstalledAppsToBottom": "Mover aplicativos não instalados para o fundo da lista de aplicativos",
|
"moveNonInstalledAppsToBottom": "Mover aplicativos não instalados para o fundo da lista de aplicativos",
|
||||||
AQUI
|
|
||||||
"gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
|
"gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
|
||||||
"about": "Sobre",
|
"about": "Sobre",
|
||||||
"requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
|
"requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
|
||||||
@@ -288,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku não está rodando",
|
"shizukuBinderNotFound": "Shizuku não está rodando",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remover aplicativo?",
|
"one": "Remover aplicativo?",
|
||||||
"other": "Remover aplicativos?"
|
"other": "Remover aplicativos?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Открепить",
|
"unpinFromTop": "Открепить",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
|
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
|
||||||
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, некорректная — из-за неудачных обновлений или других проблем",
|
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, некорректная — из-за неудачных обновлений или других проблем",
|
||||||
|
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "Поделиться выбранными URL-адресами приложений",
|
"shareSelectedAppURLs": "Поделиться выбранными URL-адресами приложений",
|
||||||
"resetInstallStatus": "Сбросить статус установки",
|
"resetInstallStatus": "Сбросить статус установки",
|
||||||
"more": "Ещё",
|
"more": "Ещё",
|
||||||
@@ -289,6 +291,8 @@
|
|||||||
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
|
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
|
||||||
"useSystemFont": "Использовать системный шрифт",
|
"useSystemFont": "Использовать системный шрифт",
|
||||||
"systemFontError": "Ошибка загрузки системного шрифта: {}",
|
"systemFontError": "Ошибка загрузки системного шрифта: {}",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Удалить приложение?",
|
"one": "Удалить приложение?",
|
||||||
"other": "Удалить приложения?"
|
"other": "Удалить приложения?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Avnåla",
|
"unpinFromTop": "Avnåla",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Återställ Installationsstatus för valda Appar?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Dela Valda Appars URL:er",
|
||||||
"resetInstallStatus": "Återställ Installationstatus",
|
"resetInstallStatus": "Återställ Installationstatus",
|
||||||
"more": "Mer",
|
"more": "Mer",
|
||||||
@@ -273,6 +275,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Ta Bort App?",
|
"one": "Ta Bort App?",
|
||||||
"other": "Ta Bort Appar?"
|
"other": "Ta Bort Appar?"
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Üstten Kaldır",
|
"unpinFromTop": "Üstten Kaldır",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Seçilen Uygulamaların Yükleme Durumunu Sıfırlamak İstiyor musunuz?",
|
"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.",
|
"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ş",
|
"shareSelectedAppURLs": "Seçilen Uygulama URL'larını Paylaş",
|
||||||
"resetInstallStatus": "Yükleme Durumunu Sıfırla",
|
"resetInstallStatus": "Yükleme Durumunu Sıfırla",
|
||||||
"more": "Daha Fazla",
|
"more": "Daha Fazla",
|
||||||
@@ -287,6 +289,8 @@
|
|||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku is not running",
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Uygulamayı Kaldır?",
|
"one": "Uygulamayı Kaldır?",
|
||||||
"other": "Uygulamaları Kaldır?"
|
"other": "Uygulamaları Kaldır?"
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
"unexpectedError": "Lỗi không mong đợi",
|
"unexpectedError": "Lỗi không mong đợi",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"and": "và",
|
"and": "và",
|
||||||
"githubPATLabel": "Mã thông báo truy cập cá nhân GitHub (Tăng tốc độ giới hạn)",
|
"githubPATLabel": "GitHub Token (Tăng tốc độ, giới hạn)",
|
||||||
"includePrereleases": "Bao gồm các bản phát hành trước",
|
"includePrereleases": "Bao gồm các bản phát hành trước",
|
||||||
"fallbackToOlderReleases": "Dự phòng về bản phát hành cũ hơn",
|
"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",
|
"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",
|
"cancelled": "Đã hủy",
|
||||||
"appAlreadyAdded": "Ứng dụng được thêm rồi",
|
"appAlreadyAdded": "Ứng dụng được thêm rồi",
|
||||||
"alreadyUpToDateQuestion": "Ứng dụng đã được cập nhật?",
|
"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",
|
"appSourceURL": "URL nguồn ứng dụng",
|
||||||
"error": "Lỗi",
|
"error": "Lỗi",
|
||||||
"add": "Thêm",
|
"add": "Thêm",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"noApps": "Không có ứng dụng",
|
"noApps": "Không có ứng dụng",
|
||||||
"noAppsForFilter": "Không có ứng dụng cho bộ lọc",
|
"noAppsForFilter": "Không có ứng dụng cho bộ lọc",
|
||||||
"byX": "Bởi {}",
|
"byX": "Bởi {}",
|
||||||
"percentProgress": "Tiến triển: {}%",
|
"percentProgress": "Đang tải {}%",
|
||||||
"pleaseWait": "Vui lòng chờ",
|
"pleaseWait": "Vui lòng chờ",
|
||||||
"updateAvailable": "Có sẵn bản cập nhật",
|
"updateAvailable": "Có sẵn bản cập nhật",
|
||||||
"estimateInBracketsShort": "(Ước lượng.)",
|
"estimateInBracketsShort": "(Ước lượng.)",
|
||||||
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "Bỏ ghim khỏi đầu trang",
|
"unpinFromTop": "Bỏ ghim khỏi đầu trang",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Đặt lại trạng thái cài đặt cho ứng dụng đã chọn?",
|
"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.",
|
"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",
|
"shareSelectedAppURLs": "Chia sẻ URL ứng dụng đã chọn",
|
||||||
"resetInstallStatus": "Đặt lại trạng thái cài đặt",
|
"resetInstallStatus": "Đặt lại trạng thái cài đặt",
|
||||||
"more": "Nhiều hơn",
|
"more": "Nhiều hơn",
|
||||||
@@ -88,10 +90,10 @@
|
|||||||
"importExport": "Nhập/Xuất",
|
"importExport": "Nhập/Xuất",
|
||||||
"settings": "Cài đặt",
|
"settings": "Cài đặt",
|
||||||
"exportedTo": "Đã xuất sang {}",
|
"exportedTo": "Đã xuất sang {}",
|
||||||
"obtainiumExport": "Xuất Obtainium",
|
"obtainiumExport": "Xuất",
|
||||||
"invalidInput": "Đầu vào không hợp lệ",
|
"invalidInput": "Đầu vào không hợp lệ",
|
||||||
"importedX": "Đã nhập {}",
|
"importedX": "Đã nhập {}",
|
||||||
"obtainiumImport": "Nhập Obtainium",
|
"obtainiumImport": "Nhập",
|
||||||
"importFromURLList": "Nhập từ danh sách URL",
|
"importFromURLList": "Nhập từ danh sách URL",
|
||||||
"searchQuery": "Truy vấn tìm kiếm",
|
"searchQuery": "Truy vấn tìm kiếm",
|
||||||
"appURLList": "Danh sách URL ứng dụng",
|
"appURLList": "Danh sách URL ứng dụng",
|
||||||
@@ -120,13 +122,13 @@
|
|||||||
"appSortOrder": "Thứ tự sắp xếp",
|
"appSortOrder": "Thứ tự sắp xếp",
|
||||||
"ascending": "Tăng dần",
|
"ascending": "Tăng dần",
|
||||||
"descending": "Giảm dần",
|
"descending": "Giảm dần",
|
||||||
"bgUpdateCheckInterval": "Khoảng thời gian kiểm tra cập nhật nền",
|
"bgUpdateCheckInterval": "Thời gian tự động kiểm tra cập nhật",
|
||||||
"neverManualOnly": "Không bao giờ - Chỉ thủ công",
|
"neverManualOnly": "Không bao giờ",
|
||||||
"appearance": "Hiển thị",
|
"appearance": "Hiển thị",
|
||||||
"showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem Ứng dụng",
|
"showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem chi tiết Ứng dụng",
|
||||||
"pinUpdates": "Ghim nội dung cập nhật lên đầu chế độ xem Ứng dụng",
|
"pinUpdates": "Chuyển ứng dụng có phiên bản mới lên đầu danh sách",
|
||||||
"updates": "Cập nhật",
|
"updates": "Cập nhật",
|
||||||
"sourceSpecific": "Nguồn cụ thể",
|
"sourceSpecific": "Cài đặt Nguồn",
|
||||||
"appSource": "Nguồn ứng dụng",
|
"appSource": "Nguồn ứng dụng",
|
||||||
"noLogs": "Không có nhật ký",
|
"noLogs": "Không có nhật ký",
|
||||||
"appLogs": "Nhật ký ứng dụng",
|
"appLogs": "Nhật ký ứng dụng",
|
||||||
@@ -219,8 +221,8 @@
|
|||||||
"dontShowAgain": "Đừng hiển thị thông tin này nữa",
|
"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'",
|
"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",
|
"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",
|
"moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách",
|
||||||
"gitlabPATLabel": "Mã thông báo truy cập cá nhân GitLab\n(Cho phép tìm kiếm và khám phá APK tốt hơn)",
|
"gitlabPATLabel": "GitLab Token\n(Cho phép tìm kiếm và lọc APK tốt hơn)",
|
||||||
"about": "Giới thiệu",
|
"about": "Giới thiệu",
|
||||||
"requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Cài đặt)",
|
"requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Cài đặt)",
|
||||||
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
|
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
|
||||||
@@ -241,9 +243,9 @@
|
|||||||
"appsPossiblyUpdated": "Đã cố gắng cập nhật ứng dụng",
|
"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",
|
"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 {}.",
|
"xWasPossiblyUpdatedToY": "{} có thể đã được cập nhật thành {}.",
|
||||||
"enableBackgroundUpdates": "Bật cập nhật nền",
|
"enableBackgroundUpdates": "Tự động cập nhật trong 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.",
|
"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 nền chỉ có thể được xác định khi mở Obtainium.",
|
"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'",
|
"verifyLatestTag": "Xác minh thẻ 'mới nhất'",
|
||||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
|
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
|
||||||
"filterByLinkText": "Filter links by link text",
|
"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",
|
"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",
|
"highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
|
||||||
"pickExportDir": "Chọn thư mục xuất",
|
"pickExportDir": "Chọn thư mục xuất",
|
||||||
"autoExportOnChanges": "Tự động xuất khi thay đổi",
|
"autoExportOnChanges": "Tự động xuất",
|
||||||
"includeSettings": "Include settings",
|
"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",
|
"filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
|
||||||
"trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
|
"trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
|
||||||
"dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
|
"dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
|
||||||
@@ -278,12 +280,12 @@
|
|||||||
"downloadingXNotifChannel": "Đang tải xuống {}",
|
"downloadingXNotifChannel": "Đang tải xuống {}",
|
||||||
"completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
|
"completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
|
||||||
"checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật",
|
"checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật",
|
||||||
"onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật",
|
"onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra cập nhật các ứng dụng đã cài đặt và Chỉ-Theo dõi",
|
||||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||||
"selectX": "Select {}",
|
"selectX": "Select {}",
|
||||||
"parallelDownloads": "Allow parallel downloads",
|
"parallelDownloads": "Cho phép tải đa luồng",
|
||||||
"installMethod": "Installation method",
|
"installMethod": "Phương thức cài đặt",
|
||||||
"normal": "Normal",
|
"normal": "Mặc định",
|
||||||
"shizuku": "Shizuku",
|
"shizuku": "Shizuku",
|
||||||
"root": "Root",
|
"root": "Root",
|
||||||
"shizukuBinderNotFound": "Shizuku chưa khởi động",
|
"shizukuBinderNotFound": "Shizuku chưa khởi động",
|
||||||
|
@@ -73,6 +73,8 @@
|
|||||||
"unpinFromTop": "取消置顶",
|
"unpinFromTop": "取消置顶",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "是否重置选中应用的安装状态?",
|
"resetInstallStatusForSelectedAppsQuestion": "是否重置选中应用的安装状态?",
|
||||||
"installStatusOfXWillBeResetExplanation": "选中应用的安装状态将会被重置。\n\n当更新安装失败或其他问题导致 Obtainium 中的应用版本显示错误时,可以尝试通过此方法解决。",
|
"installStatusOfXWillBeResetExplanation": "选中应用的安装状态将会被重置。\n\n当更新安装失败或其他问题导致 Obtainium 中的应用版本显示错误时,可以尝试通过此方法解决。",
|
||||||
|
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||||
|
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||||
"shareSelectedAppURLs": "分享选中应用的 URL",
|
"shareSelectedAppURLs": "分享选中应用的 URL",
|
||||||
"resetInstallStatus": "重置安装状态",
|
"resetInstallStatus": "重置安装状态",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
@@ -289,6 +291,8 @@
|
|||||||
"shizukuBinderNotFound": "未发现兼容的 Shizuku 服务",
|
"shizukuBinderNotFound": "未发现兼容的 Shizuku 服务",
|
||||||
"useSystemFont": "使用系统字体",
|
"useSystemFont": "使用系统字体",
|
||||||
"systemFontError": "加载系统字体出错:{}",
|
"systemFontError": "加载系统字体出错:{}",
|
||||||
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
|
"requestHeader": "Request header",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "是否删除应用?",
|
"one": "是否删除应用?",
|
||||||
"other": "是否删除应用?"
|
"other": "是否删除应用?"
|
||||||
|
@@ -5,17 +5,19 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class APKCombo extends AppSource {
|
class APKCombo extends AppSource {
|
||||||
APKCombo() {
|
APKCombo() {
|
||||||
host = 'apkcombo.com';
|
hosts = ['apkcombo.com'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
var match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -26,18 +28,19 @@ class APKCombo extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
Map<String, dynamic> additionalSettings,
|
||||||
bool forAPKDownload = false}) async {
|
{bool forAPKDownload = false}) async {
|
||||||
return {
|
return {
|
||||||
"User-Agent": "curl/8.0.1",
|
"User-Agent": "curl/8.0.1",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Host": "$host"
|
"Host": hosts[0]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
|
Future<List<MapEntry<String, String>>> getApkUrls(
|
||||||
var res = await sourceRequest('$standardUrl/download/apk');
|
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||||
|
var res = await sourceRequest('$standardUrl/download/apk', {});
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@@ -70,9 +73,9 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(
|
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||||
String apkUrl, String standardUrl) async {
|
Map<String, dynamic> additionalSettings) async {
|
||||||
var freshURLs = await getApkUrls(standardUrl);
|
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
|
||||||
var path2Match = Uri.parse(apkUrl).path;
|
var path2Match = Uri.parse(apkUrl).path;
|
||||||
for (var url in freshURLs) {
|
for (var url in freshURLs) {
|
||||||
if (Uri.parse(url.value).path == path2Match) {
|
if (Uri.parse(url.value).path == path2Match) {
|
||||||
@@ -88,7 +91,7 @@ class APKCombo extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String appId = (await tryInferringAppId(standardUrl))!;
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
var preres = await sourceRequest(standardUrl);
|
var preres = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (preres.statusCode != 200) {
|
if (preres.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(preres);
|
throw getObtainiumHttpError(preres);
|
||||||
}
|
}
|
||||||
@@ -112,7 +115,9 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, await getApkUrls(standardUrl), AppNames(author, appName),
|
version,
|
||||||
|
await getApkUrls(standardUrl, additionalSettings),
|
||||||
|
AppNames(author, appName),
|
||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class APKMirror extends AppSource {
|
class APKMirror extends AppSource {
|
||||||
APKMirror() {
|
APKMirror() {
|
||||||
host = 'apkmirror.com';
|
hosts = ['apkmirror.com'];
|
||||||
enforceTrackOnly = true;
|
enforceTrackOnly = true;
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
@@ -32,13 +32,14 @@ class APKMirror extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx =
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+');
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -58,7 +59,7 @@ class APKMirror extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await sourceRequest('$standardUrl/feed');
|
Response res = await sourceRequest('$standardUrl/feed', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var items = parse(res.body).querySelectorAll('item');
|
var items = parse(res.body).querySelectorAll('item');
|
||||||
dynamic targetRelease;
|
dynamic targetRelease;
|
||||||
@@ -84,7 +85,7 @@ class APKMirror extends AppSource {
|
|||||||
dateString != null ? HttpDate.parse('$dateString GMT') : null;
|
dateString != null ? HttpDate.parse('$dateString GMT') : null;
|
||||||
String? version = titleString
|
String? version = titleString
|
||||||
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
||||||
RegExp(' by ').firstMatch(titleString)?.start ?? 0)
|
RegExp(' by ').allMatches(titleString).last.start)
|
||||||
.trim();
|
.trim();
|
||||||
if (version == null || version.isEmpty) {
|
if (version == null || version.isEmpty) {
|
||||||
version = titleString;
|
version = titleString;
|
||||||
|
@@ -20,26 +20,28 @@ parseDateTimeMMMddCommayyyy(String? dateString) {
|
|||||||
|
|
||||||
class APKPure extends AppSource {
|
class APKPure extends AppSource {
|
||||||
APKPure() {
|
APKPure() {
|
||||||
host = 'apkpure.com';
|
hosts = ['apkpure.net', 'apkpure.com'];
|
||||||
allowSubDomains = true;
|
allowSubDomains = true;
|
||||||
naiveStandardVersionDetection = true;
|
naiveStandardVersionDetection = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegExB =
|
RegExp standardUrlRegExB = RegExp(
|
||||||
RegExp('^https?://m.$host/+[^/]+/+[^/]+(/+[^/]+)?');
|
'^https?://m.${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?',
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url = 'https://$host${Uri.parse(url).path}';
|
url = 'https://${getSourceRegex(hosts)}${Uri.parse(url).path}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA =
|
RegExp standardUrlRegExA = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+(/+[^/]+)?');
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?',
|
||||||
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -55,8 +57,8 @@ class APKPure extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
String appId = (await tryInferringAppId(standardUrl))!;
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
String host = Uri.parse(standardUrl).host;
|
String host = Uri.parse(standardUrl).host;
|
||||||
var res = await sourceRequest('$standardUrl/download');
|
var res = await sourceRequest('$standardUrl/download', additionalSettings);
|
||||||
var resChangelog = await sourceRequest(standardUrl);
|
var resChangelog = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode == 200 && resChangelog.statusCode == 200) {
|
if (res.statusCode == 200 && resChangelog.statusCode == 200) {
|
||||||
var html = parse(res.body);
|
var html = parse(res.body);
|
||||||
var htmlChangelog = parse(resChangelog.body);
|
var htmlChangelog = parse(resChangelog.body);
|
||||||
@@ -69,7 +71,8 @@ class APKPure extends AppSource {
|
|||||||
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
|
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
|
||||||
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||||
List<MapEntry<String, String>> apkUrls = [
|
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
|
String author = html
|
||||||
.querySelector('span.info-sdk')
|
.querySelector('span.info-sdk')
|
||||||
|
@@ -6,7 +6,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class Aptoide extends AppSource {
|
class Aptoide extends AppSource {
|
||||||
Aptoide() {
|
Aptoide() {
|
||||||
host = 'aptoide.com';
|
hosts = ['aptoide.com'];
|
||||||
name = 'Aptoide';
|
name = 'Aptoide';
|
||||||
allowSubDomains = true;
|
allowSubDomains = true;
|
||||||
naiveStandardVersionDetection = true;
|
naiveStandardVersionDetection = true;
|
||||||
@@ -14,22 +14,26 @@ class Aptoide extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
{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 {
|
Future<Map<String, dynamic>> getAppDetailsJSON(
|
||||||
var res = await sourceRequest(standardUrl);
|
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||||
|
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@@ -40,8 +44,8 @@ class Aptoide extends AppSource {
|
|||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var res2 =
|
var res2 = await sourceRequest(
|
||||||
await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id');
|
'https://ws2.aptoide.com/api/7/getApp/app_id/$id', additionalSettings);
|
||||||
if (res2.statusCode != 200) {
|
if (res2.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@@ -53,7 +57,7 @@ class Aptoide extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var appDetails = await getAppDetailsJSON(standardUrl);
|
var appDetails = await getAppDetailsJSON(standardUrl, additionalSettings);
|
||||||
String appName = appDetails['name'] ?? tr('app');
|
String appName = appDetails['name'] ?? tr('app');
|
||||||
String author = appDetails['developer']?['name'] ?? name;
|
String author = appDetails['developer']?['name'] ?? name;
|
||||||
String? dateStr = appDetails['updated'];
|
String? dateStr = appDetails['updated'];
|
||||||
|
@@ -5,7 +5,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
class Codeberg extends AppSource {
|
class Codeberg extends AppSource {
|
||||||
GitHub gh = GitHub();
|
GitHub gh = GitHub();
|
||||||
Codeberg() {
|
Codeberg() {
|
||||||
host = 'codeberg.org';
|
hosts = ['codeberg.org'];
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems =
|
additionalSourceAppSpecificSettingFormItems =
|
||||||
gh.additionalSourceAppSpecificSettingFormItems;
|
gh.additionalSourceAppSpecificSettingFormItems;
|
||||||
@@ -16,12 +16,14 @@ class Codeberg extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,7 +37,7 @@ class Codeberg extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||||
(bool useTagUrl) async {
|
(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);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ class Codeberg extends AppSource {
|
|||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
{Map<String, dynamic> querySettings = const {}}) async {
|
||||||
return gh.searchCommon(
|
return gh.searchCommon(
|
||||||
query,
|
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',
|
'data',
|
||||||
querySettings: querySettings);
|
querySettings: querySettings);
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class FDroid extends AppSource {
|
class FDroid extends AppSource {
|
||||||
FDroid() {
|
FDroid() {
|
||||||
host = 'f-droid.org';
|
hosts = ['f-droid.org'];
|
||||||
name = tr('fdroid');
|
name = tr('fdroid');
|
||||||
naiveStandardVersionDetection = true;
|
naiveStandardVersionDetection = true;
|
||||||
canSearch = true;
|
canSearch = true;
|
||||||
@@ -37,20 +37,22 @@ class FDroid extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegExB =
|
RegExp standardUrlRegExB = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$host/+[^/]+/+packages/+[^/]+');
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url =
|
url =
|
||||||
'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
|
'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.last}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA =
|
RegExp standardUrlRegExA = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$host/+packages/+[^/]+');
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
|
||||||
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -67,7 +69,8 @@ class FDroid extends AppSource {
|
|||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
String host = Uri.parse(standardUrl).host;
|
String host = Uri.parse(standardUrl).host;
|
||||||
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
await sourceRequest(
|
||||||
|
'https://$host/api/v1/packages/$appId', additionalSettings),
|
||||||
'https://$host/repo/$appId',
|
'https://$host/repo/$appId',
|
||||||
standardUrl,
|
standardUrl,
|
||||||
name,
|
name,
|
||||||
@@ -84,18 +87,32 @@ class FDroid extends AppSource {
|
|||||||
if (!hostChanged) {
|
if (!hostChanged) {
|
||||||
try {
|
try {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml');
|
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
||||||
String author = res.body
|
additionalSettings);
|
||||||
.split('\n')
|
var lines = res.body.split('\n');
|
||||||
.where((l) => l.startsWith('AuthorName: '))
|
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
||||||
.first
|
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(': ')
|
.split(': ')
|
||||||
.sublist(1)
|
.sublist(1)
|
||||||
.join(': ');
|
.join(': ')
|
||||||
details.names.author = author;
|
.replaceFirst('/blob/', '/raw/'),
|
||||||
|
additionalSettings))
|
||||||
|
.body;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fail silently
|
// Fail silently
|
||||||
}
|
}
|
||||||
|
if ((details.changeLog?.length ?? 0) > 2048) {
|
||||||
|
details.changeLog = '${details.changeLog!.substring(0, 2048)}...';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
@@ -104,7 +121,7 @@ class FDroid extends AppSource {
|
|||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(String query,
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
{Map<String, dynamic> querySettings = const {}}) async {
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
|
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||||
|
@@ -59,7 +59,7 @@ class FDroidRepo extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
url = removeQueryParamsFromUrl(standardizeUrl(url));
|
url = removeQueryParamsFromUrl(standardizeUrl(url));
|
||||||
var res = await sourceRequest('$url/index.xml');
|
var res = await sourceRequest('$url/index.xml', {});
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var body = parse(res.body);
|
var body = parse(res.body);
|
||||||
Map<String, List<String>> results = {};
|
Map<String, List<String>> results = {};
|
||||||
@@ -117,7 +117,8 @@ class FDroidRepo extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}');
|
'$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}',
|
||||||
|
additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var body = parse(res.body);
|
var body = parse(res.body);
|
||||||
var foundApps = body.querySelectorAll('application').where((element) {
|
var foundApps = body.querySelectorAll('application').where((element) {
|
||||||
|
@@ -14,7 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
|
|
||||||
class GitHub extends AppSource {
|
class GitHub extends AppSource {
|
||||||
GitHub() {
|
GitHub() {
|
||||||
host = 'github.com';
|
hosts = ['github.com'];
|
||||||
appIdInferIsOptional = true;
|
appIdInferIsOptional = true;
|
||||||
|
|
||||||
sourceConfigSettingFormItems = [
|
sourceConfigSettingFormItems = [
|
||||||
@@ -108,7 +108,8 @@ class GitHub extends AppSource {
|
|||||||
for (var path in possibleBuildGradleLocations) {
|
for (var path in possibleBuildGradleLocations) {
|
||||||
try {
|
try {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path');
|
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
||||||
|
additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
try {
|
try {
|
||||||
var body = jsonDecode(res.body);
|
var body = jsonDecode(res.body);
|
||||||
@@ -149,18 +150,20 @@ class GitHub extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
Map<String, dynamic> additionalSettings,
|
||||||
bool forAPKDownload = false}) async {
|
{bool forAPKDownload = false}) async {
|
||||||
var token = await getTokenIfAny(additionalSettings);
|
var token = await getTokenIfAny(additionalSettings);
|
||||||
var headers = <String, String>{};
|
var headers = <String, String>{};
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
@@ -203,11 +206,11 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
|
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
|
||||||
'https://api.$host';
|
'https://api.${hosts[0]}';
|
||||||
|
|
||||||
Future<String> convertStandardUrlToAPIUrl(
|
Future<String> convertStandardUrlToAPIUrl(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
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
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
@@ -238,7 +241,8 @@ class GitHub extends AppSource {
|
|||||||
if (verifyLatestTag) {
|
if (verifyLatestTag) {
|
||||||
var temp = requestUrl.split('?');
|
var temp = requestUrl.split('?');
|
||||||
Response res = await sourceRequest(
|
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 (res.statusCode != 200) {
|
||||||
if (onHttpErrorCode != null) {
|
if (onHttpErrorCode != null) {
|
||||||
onHttpErrorCode(res);
|
onHttpErrorCode(res);
|
||||||
@@ -247,7 +251,7 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
latestRelease = jsonDecode(res.body);
|
latestRelease = jsonDecode(res.body);
|
||||||
}
|
}
|
||||||
Response res = await sourceRequest(requestUrl);
|
Response res = await sourceRequest(requestUrl, additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
if (latestRelease != null) {
|
if (latestRelease != null) {
|
||||||
@@ -424,7 +428,7 @@ class GitHub extends AppSource {
|
|||||||
String query, String requestUrl, String rootProp,
|
String query, String requestUrl, String rootProp,
|
||||||
{Function(Response)? onHttpErrorCode,
|
{Function(Response)? onHttpErrorCode,
|
||||||
Map<String, dynamic> querySettings = const {}}) async {
|
Map<String, dynamic> querySettings = const {}}) async {
|
||||||
Response res = await sourceRequest(requestUrl);
|
Response res = await sourceRequest(requestUrl, {});
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
int minStarCount = querySettings['minStarCount'] != null
|
int minStarCount = querySettings['minStarCount'] != null
|
||||||
? int.parse(querySettings['minStarCount'])
|
? int.parse(querySettings['minStarCount'])
|
||||||
|
@@ -13,7 +13,7 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
|
|
||||||
class GitLab extends AppSource {
|
class GitLab extends AppSource {
|
||||||
GitLab() {
|
GitLab() {
|
||||||
host = 'gitlab.com';
|
hosts = ['gitlab.com'];
|
||||||
canSearch = true;
|
canSearch = true;
|
||||||
|
|
||||||
sourceConfigSettingFormItems = [
|
sourceConfigSettingFormItems = [
|
||||||
@@ -52,12 +52,14 @@ class GitLab extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
||||||
@@ -81,15 +83,15 @@ class GitLab extends AppSource {
|
|||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(String query,
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
{Map<String, dynamic> querySettings = const {}}) async {
|
||||||
var url =
|
var url =
|
||||||
'https://$host/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
||||||
var res = await sourceRequest(url);
|
var res = await sourceRequest(url, {});
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
var json = jsonDecode(res.body) as List<dynamic>;
|
var json = jsonDecode(res.body) as List<dynamic>;
|
||||||
Map<String, List<String>> results = {};
|
Map<String, List<String>> results = {};
|
||||||
for (var element in json) {
|
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['name_with_namespace'],
|
||||||
element['description'] ?? tr('noDescription')
|
element['description'] ?? tr('noDescription')
|
||||||
];
|
];
|
||||||
@@ -113,7 +115,8 @@ class GitLab extends AppSource {
|
|||||||
if (PAT != null) {
|
if (PAT != null) {
|
||||||
var names = GitHub().getAppNames(standardUrl);
|
var names = GitHub().getAppNames(standardUrl);
|
||||||
Response res = await sourceRequest(
|
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) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@@ -148,7 +151,8 @@ class GitLab extends AppSource {
|
|||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Response res = await sourceRequest('$standardUrl/-/tags?format=atom');
|
Response res = await sourceRequest(
|
||||||
|
'$standardUrl/-/tags?format=atom', additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,8 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
|||||||
.toList();
|
.toList();
|
||||||
String absoluteUrl;
|
String absoluteUrl;
|
||||||
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
||||||
|
absoluteUrl = '${referenceAbsoluteUrl.origin}$ambiguousUrl';
|
||||||
|
} else if (currPathSegments.isEmpty) {
|
||||||
absoluteUrl = '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
absoluteUrl = '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
||||||
} else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
|
} else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
|
||||||
absoluteUrl =
|
absoluteUrl =
|
||||||
@@ -139,7 +141,38 @@ class HTML extends AppSource {
|
|||||||
],
|
],
|
||||||
finalStepFormitems[0],
|
finalStepFormitems[0],
|
||||||
...commonFormItems,
|
...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'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
];
|
];
|
||||||
overrideVersionDetectionFormDefault('noVersionDetection',
|
overrideVersionDetectionFormDefault('noVersionDetection',
|
||||||
disableStandard: false, disableRelDate: true);
|
disableStandard: false, disableRelDate: true);
|
||||||
@@ -147,12 +180,25 @@ class HTML extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
Map<String, dynamic> additionalSettings,
|
||||||
bool forAPKDownload = false}) async {
|
{bool forAPKDownload = false}) async {
|
||||||
return {
|
if (additionalSettings.isNotEmpty) {
|
||||||
"User-Agent":
|
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
|
||||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
|
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
|
@override
|
||||||
@@ -233,7 +279,8 @@ class HTML extends AppSource {
|
|||||||
.where((l) => l['customLinkFilterRegex'].isNotEmpty == true)
|
.where((l) => l['customLinkFilterRegex'].isNotEmpty == true)
|
||||||
.toList();
|
.toList();
|
||||||
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
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]);
|
additionalSettings['intermediateLink'][i]);
|
||||||
if (intLinks.isEmpty) {
|
if (intLinks.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
@@ -243,7 +290,7 @@ class HTML extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var uri = Uri.parse(currentUrl);
|
var uri = Uri.parse(currentUrl);
|
||||||
Response res = await sourceRequest(currentUrl);
|
Response res = await sourceRequest(currentUrl, additionalSettings);
|
||||||
var links = await grabLinksCommon(res, additionalSettings);
|
var links = await grabLinksCommon(res, additionalSettings);
|
||||||
|
|
||||||
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true) {
|
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true) {
|
||||||
@@ -255,16 +302,15 @@ class HTML extends AppSource {
|
|||||||
}
|
}
|
||||||
var rel = links.last.key;
|
var rel = links.last.key;
|
||||||
String? version;
|
String? version;
|
||||||
if (additionalSettings['supportFixedAPKURL'] != true) {
|
|
||||||
version = rel.hashCode.toString();
|
|
||||||
}
|
|
||||||
version = extractVersion(
|
version = extractVersion(
|
||||||
additionalSettings['versionExtractionRegEx'] as String?,
|
additionalSettings['versionExtractionRegEx'] as String?,
|
||||||
additionalSettings['matchGroupToUse'] as String?,
|
additionalSettings['matchGroupToUse'] as String?,
|
||||||
additionalSettings['versionExtractWholePage'] == true
|
additionalSettings['versionExtractWholePage'] == true
|
||||||
? res.body.split('\r\n').join('\n').split('\n').join('\\n')
|
? res.body.split('\r\n').join('\n').split('\n').join('\\n')
|
||||||
: rel);
|
: rel);
|
||||||
version ??= (await checkDownloadHash(rel)).toString();
|
version ??= additionalSettings['supportFixedAPKURL'] != true
|
||||||
|
? rel.hashCode.toString()
|
||||||
|
: (await checkDownloadHash(rel)).toString();
|
||||||
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
|
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
|
||||||
AppNames(uri.host, tr('app')));
|
AppNames(uri.host, tr('app')));
|
||||||
}
|
}
|
||||||
|
@@ -6,26 +6,30 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
class HuaweiAppGallery extends AppSource {
|
class HuaweiAppGallery extends AppSource {
|
||||||
HuaweiAppGallery() {
|
HuaweiAppGallery() {
|
||||||
name = 'Huawei AppGallery';
|
name = 'Huawei AppGallery';
|
||||||
host = 'appgallery.huawei.com';
|
hosts = ['appgallery.huawei.com'];
|
||||||
overrideVersionDetectionFormDefault('releaseDateAsVersion',
|
overrideVersionDetectionFormDefault('releaseDateAsVersion',
|
||||||
disableStandard: true);
|
disableStandard: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/app/[^/]+');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/app/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDlUrl(String standardUrl) =>
|
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 {
|
requestAppdlRedirect(
|
||||||
Response res = await sourceRequest(dlUrl, followRedirects: false);
|
String dlUrl, Map<String, dynamic> additionalSettings) async {
|
||||||
|
Response res =
|
||||||
|
await sourceRequest(dlUrl, additionalSettings, followRedirects: false);
|
||||||
if (res.statusCode == 200 ||
|
if (res.statusCode == 200 ||
|
||||||
res.statusCode == 302 ||
|
res.statusCode == 302 ||
|
||||||
res.statusCode == 304) {
|
res.statusCode == 304) {
|
||||||
@@ -52,7 +56,7 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
String dlUrl = getDlUrl(standardUrl);
|
String dlUrl = getDlUrl(standardUrl);
|
||||||
Response res = await requestAppdlRedirect(dlUrl);
|
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||||
return res.headers['location'] != null
|
return res.headers['location'] != null
|
||||||
? appIdFromRedirectDlUrl(res.headers['location']!)
|
? appIdFromRedirectDlUrl(res.headers['location']!)
|
||||||
: null;
|
: null;
|
||||||
@@ -64,7 +68,7 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String dlUrl = getDlUrl(standardUrl);
|
String dlUrl = getDlUrl(standardUrl);
|
||||||
Response res = await requestAppdlRedirect(dlUrl);
|
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||||
if (res.headers['location'] == null) {
|
if (res.headers['location'] == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ class IzzyOnDroid extends AppSource {
|
|||||||
late FDroid fd;
|
late FDroid fd;
|
||||||
|
|
||||||
IzzyOnDroid() {
|
IzzyOnDroid() {
|
||||||
host = 'izzysoft.de';
|
hosts = ['izzysoft.de'];
|
||||||
fd = FDroid();
|
fd = FDroid();
|
||||||
additionalSourceAppSpecificSettingFormItems =
|
additionalSourceAppSpecificSettingFormItems =
|
||||||
fd.additionalSourceAppSpecificSettingFormItems;
|
fd.additionalSourceAppSpecificSettingFormItems;
|
||||||
@@ -15,17 +15,20 @@ class IzzyOnDroid extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegExA = RegExp('^https?://android.$host/repo/apk/[^/]+');
|
RegExp standardUrlRegExA = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
RegExp standardUrlRegExB =
|
RegExp standardUrlRegExB = RegExp(
|
||||||
RegExp('^https?://apt.$host/fdroid/index/apk/[^/]+');
|
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
|
||||||
match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
match = standardUrlRegExB.firstMatch(url);
|
||||||
}
|
}
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,7 +45,8 @@ class IzzyOnDroid extends AppSource {
|
|||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest(
|
await sourceRequest(
|
||||||
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
|
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
|
||||||
|
additionalSettings),
|
||||||
'https://android.izzysoft.de/frepo/$appId',
|
'https://android.izzysoft.de/frepo/$appId',
|
||||||
standardUrl,
|
standardUrl,
|
||||||
name,
|
name,
|
||||||
|
@@ -8,6 +8,7 @@ class Jenkins extends AppSource {
|
|||||||
Jenkins() {
|
Jenkins() {
|
||||||
overrideVersionDetectionFormDefault('releaseDateAsVersion',
|
overrideVersionDetectionFormDefault('releaseDateAsVersion',
|
||||||
disableStandard: true);
|
disableStandard: true);
|
||||||
|
neverAutoSelect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String trimJobUrl(String url) {
|
String trimJobUrl(String url) {
|
||||||
@@ -16,7 +17,7 @@ class Jenkins extends AppSource {
|
|||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,8 +30,8 @@ class Jenkins extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
standardUrl = trimJobUrl(standardUrl);
|
standardUrl = trimJobUrl(standardUrl);
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
|
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
var releaseDate = json['timestamp'] == null
|
var releaseDate = json['timestamp'] == null
|
||||||
|
@@ -6,17 +6,19 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class Mullvad extends AppSource {
|
class Mullvad extends AppSource {
|
||||||
Mullvad() {
|
Mullvad() {
|
||||||
host = 'mullvad.net';
|
hosts = ['mullvad.net'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,7 +30,8 @@ class Mullvad extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest('$standardUrl/en/download/android');
|
Response res = await sourceRequest(
|
||||||
|
'$standardUrl/en/download/android', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var versions = parse(res.body)
|
var versions = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
|
@@ -5,18 +5,19 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class NeutronCode extends AppSource {
|
class NeutronCode extends AppSource {
|
||||||
NeutronCode() {
|
NeutronCode() {
|
||||||
host = 'neutroncode.com';
|
hosts = ['neutroncode.com'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx =
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$host/downloads/file/[^/]+');
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -79,7 +80,7 @@ class NeutronCode extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest(standardUrl);
|
Response res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var name = http.querySelector('.pd-title')?.innerHtml;
|
var name = http.querySelector('.pd-title')?.innerHtml;
|
||||||
@@ -92,7 +93,7 @@ class NeutronCode extends AppSource {
|
|||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? apkUrl = 'https://$host/download/$filename';
|
String? apkUrl = 'https://${hosts[0]}/download/$filename';
|
||||||
var dateStringOriginal =
|
var dateStringOriginal =
|
||||||
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
|
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
|
||||||
var dateString = dateStringOriginal != null
|
var dateString = dateStringOriginal != null
|
||||||
|
@@ -5,12 +5,12 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class Signal extends AppSource {
|
class Signal extends AppSource {
|
||||||
Signal() {
|
Signal() {
|
||||||
host = 'signal.org';
|
hosts = ['signal.org'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://${hosts[0]}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -18,8 +18,8 @@ class Signal extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('https://updates.$host/android/latest.json');
|
'https://updates.${hosts[0]}/android/latest.json', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
String? apkUrl = json['url'];
|
String? apkUrl = json['url'];
|
||||||
|
@@ -5,24 +5,27 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class SourceForge extends AppSource {
|
class SourceForge extends AppSource {
|
||||||
SourceForge() {
|
SourceForge() {
|
||||||
host = 'sourceforge.net';
|
hosts = ['sourceforge.net'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegExB = RegExp('^https?://(www\\.)?$host/p/[^/]+');
|
RegExp standardUrlRegExB = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/p/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url =
|
url =
|
||||||
'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}';
|
'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA =
|
RegExp standardUrlRegExA = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$host/projects/[^/]+');
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/projects/[^/]+',
|
||||||
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
caseSensitive: false);
|
||||||
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -30,7 +33,8 @@ class SourceForge extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest('$standardUrl/rss?path=/');
|
Response res =
|
||||||
|
await sourceRequest('$standardUrl/rss?path=/', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
var allDownloadLinks =
|
var allDownloadLinks =
|
||||||
|
@@ -8,7 +8,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
|
|
||||||
class SourceHut extends AppSource {
|
class SourceHut extends AppSource {
|
||||||
SourceHut() {
|
SourceHut() {
|
||||||
host = 'git.sr.ht';
|
hosts = ['git.sr.ht'];
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
@@ -20,12 +20,14 @@ class SourceHut extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -40,7 +42,8 @@ class SourceHut extends AppSource {
|
|||||||
String appName = standardUri.pathSegments.last;
|
String appName = standardUri.pathSegments.last;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
Response res = await sourceRequest('$standardUrl/refs/rss.xml');
|
Response res =
|
||||||
|
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
List<APKDetails> apkDetailsList = [];
|
List<APKDetails> apkDetailsList = [];
|
||||||
@@ -69,7 +72,7 @@ class SourceHut extends AppSource {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
var res2 = await sourceRequest(releasePage);
|
var res2 = await sourceRequest(releasePage, additionalSettings);
|
||||||
List<MapEntry<String, String>> apkUrls = [];
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
if (res2.statusCode == 200) {
|
if (res2.statusCode == 200) {
|
||||||
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
||||||
|
@@ -7,7 +7,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class SteamMobile extends AppSource {
|
class SteamMobile extends AppSource {
|
||||||
SteamMobile() {
|
SteamMobile() {
|
||||||
host = 'store.steampowered.com';
|
hosts = ['store.steampowered.com'];
|
||||||
name = tr('steam');
|
name = tr('steam');
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
@@ -21,7 +21,7 @@ class SteamMobile extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://${hosts[0]}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,7 +29,8 @@ class SteamMobile extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest('https://$host/mobile');
|
Response res =
|
||||||
|
await sourceRequest('https://${hosts[0]}/mobile', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var apkNamePrefix = additionalSettings['app'] as String?;
|
var apkNamePrefix = additionalSettings['app'] as String?;
|
||||||
if (apkNamePrefix == null) {
|
if (apkNamePrefix == null) {
|
||||||
|
@@ -6,13 +6,13 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class TelegramApp extends AppSource {
|
class TelegramApp extends AppSource {
|
||||||
TelegramApp() {
|
TelegramApp() {
|
||||||
host = 'telegram.org';
|
hosts = ['telegram.org'];
|
||||||
name = 'Telegram ${tr('app')}';
|
name = 'Telegram ${tr('app')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://${hosts[0]}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -20,7 +20,8 @@ class TelegramApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest('https://t.me/s/TAndroidAPK');
|
Response res =
|
||||||
|
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var messages =
|
var messages =
|
||||||
|
@@ -6,29 +6,33 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class Uptodown extends AppSource {
|
class Uptodown extends AppSource {
|
||||||
Uptodown() {
|
Uptodown() {
|
||||||
host = 'uptodown.com';
|
hosts = ['uptodown.com'];
|
||||||
allowSubDomains = true;
|
allowSubDomains = true;
|
||||||
naiveStandardVersionDetection = true;
|
naiveStandardVersionDetection = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
|
RegExp standardUrlRegEx = RegExp(
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||||
|
caseSensitive: false);
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
return '${url.substring(0, match.end)}/android/download';
|
return '${match.group(0)!}/android/download';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
{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 {
|
Future<Map<String, String?>> getAppDetailsFromPage(
|
||||||
var res = await sourceRequest(standardUrl);
|
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||||
|
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,8 @@ class Uptodown extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var appDetails = await getAppDetailsFromPage(standardUrl);
|
var appDetails =
|
||||||
|
await getAppDetailsFromPage(standardUrl, additionalSettings);
|
||||||
var version = appDetails['version'];
|
var version = appDetails['version'];
|
||||||
var apkUrl = appDetails['apkUrl'];
|
var apkUrl = appDetails['apkUrl'];
|
||||||
var appId = appDetails['appId'];
|
var appId = appDetails['appId'];
|
||||||
@@ -82,9 +87,9 @@ class Uptodown extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(
|
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||||
String apkUrl, String standardUrl) async {
|
Map<String, dynamic> additionalSettings) async {
|
||||||
var res = await sourceRequest(apkUrl);
|
var res = await sourceRequest(apkUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@@ -94,6 +99,6 @@ class Uptodown extends AppSource {
|
|||||||
if (finalUrlKey == null) {
|
if (finalUrlKey == null) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
return 'https://dw.$host/dwn/$finalUrlKey';
|
return 'https://dw.${hosts[0]}/dwn/$finalUrlKey';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,31 +1,33 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class VLC extends AppSource {
|
class VLC extends AppSource {
|
||||||
VLC() {
|
VLC() {
|
||||||
host = 'videolan.org';
|
hosts = ['videolan.org'];
|
||||||
}
|
}
|
||||||
get dwUrlBase => 'https://get.$host/vlc-android/';
|
get dwUrlBase => 'https://get.${hosts[0]}/vlc-android/';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
Map<String, dynamic> additionalSettings,
|
||||||
bool forAPKDownload = false}) =>
|
{bool forAPKDownload = false}) async {
|
||||||
HTML().getRequestHeaders(
|
return {
|
||||||
additionalSettings: additionalSettings,
|
"User-Agent":
|
||||||
forAPKDownload: forAPKDownload);
|
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://${hosts[0]}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestVersion(String standardUrl) async {
|
Future<String?> getLatestVersion(
|
||||||
Response res = await sourceRequest(dwUrlBase);
|
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||||
|
Response res = await sourceRequest(dwUrlBase, additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var dwLinks = parse(res.body)
|
var dwLinks = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
@@ -77,9 +79,9 @@ class VLC extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(
|
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||||
String apkUrl, String standardUrl) async {
|
Map<String, dynamic> additionalSettings) async {
|
||||||
Response res = await sourceRequest(apkUrl);
|
Response res = await sourceRequest(apkUrl, additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
String? apkUrl =
|
String? apkUrl =
|
||||||
parse(res.body).querySelector('#alt_link')?.attributes['href'];
|
parse(res.body).querySelector('#alt_link')?.attributes['href'];
|
||||||
|
@@ -5,20 +5,21 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class WhatsApp extends AppSource {
|
class WhatsApp extends AppSource {
|
||||||
WhatsApp() {
|
WhatsApp() {
|
||||||
host = 'whatsapp.com';
|
hosts = ['whatsapp.com'];
|
||||||
overrideVersionDetectionFormDefault('noVersionDetection',
|
overrideVersionDetectionFormDefault('noVersionDetection',
|
||||||
disableStandard: true, disableRelDate: true);
|
disableStandard: true, disableRelDate: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://${hosts[0]}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(
|
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||||
String apkUrl, String standardUrl) async {
|
Map<String, dynamic> additionalSettings) async {
|
||||||
Response res = await sourceRequest('$standardUrl/android');
|
Response res =
|
||||||
|
await sourceRequest('$standardUrl/android', additionalSettings);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var targetLinks = parse(res.body)
|
var targetLinks = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
@@ -42,8 +43,8 @@ class WhatsApp extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
// This is a CDN link that is consistent per version
|
// This is a CDN link that is consistent per version
|
||||||
// But it has query params that change constantly
|
// But it has query params that change constantly
|
||||||
Uri apkUri =
|
Uri apkUri = Uri.parse(await apkUrlPrefetchModifier(
|
||||||
Uri.parse(await apkUrlPrefetchModifier(standardUrl, standardUrl));
|
standardUrl, standardUrl, additionalSettings));
|
||||||
var unusableApkUrl = '${apkUri.origin}/${apkUri.path}';
|
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
|
// 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
|
// See #357 for why we can't scrape the version number directly
|
||||||
|
@@ -13,6 +13,7 @@ abstract class GeneratedFormItem {
|
|||||||
late dynamic defaultValue;
|
late dynamic defaultValue;
|
||||||
List<dynamic> additionalValidators;
|
List<dynamic> additionalValidators;
|
||||||
dynamic ensureType(dynamic val);
|
dynamic ensureType(dynamic val);
|
||||||
|
GeneratedFormItem clone();
|
||||||
|
|
||||||
GeneratedFormItem(this.key,
|
GeneratedFormItem(this.key,
|
||||||
{this.label = 'Input',
|
{this.label = 'Input',
|
||||||
@@ -44,6 +45,20 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
String ensureType(val) {
|
String ensureType(val) {
|
||||||
return val.toString();
|
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 {
|
class GeneratedFormDropdown extends GeneratedFormItem {
|
||||||
@@ -64,6 +79,20 @@ class GeneratedFormDropdown extends GeneratedFormItem {
|
|||||||
String ensureType(val) {
|
String ensureType(val) {
|
||||||
return val.toString();
|
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 {
|
class GeneratedFormSwitch extends GeneratedFormItem {
|
||||||
@@ -79,6 +108,15 @@ class GeneratedFormSwitch extends GeneratedFormItem {
|
|||||||
bool ensureType(val) {
|
bool ensureType(val) {
|
||||||
return val == true || val == 'true';
|
return val == true || val == 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
GeneratedFormSwitch clone() {
|
||||||
|
return GeneratedFormSwitch(key,
|
||||||
|
label: label,
|
||||||
|
belowWidgets: belowWidgets,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
additionalValidators: List.from(additionalValidators));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedFormTagInput extends GeneratedFormItem {
|
class GeneratedFormTagInput extends GeneratedFormItem {
|
||||||
@@ -103,6 +141,20 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
|||||||
Map<String, MapEntry<int, bool>> ensureType(val) {
|
Map<String, MapEntry<int, bool>> ensureType(val) {
|
||||||
return val is Map<String, MapEntry<int, bool>> ? 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(
|
typedef OnValueChanges = void Function(
|
||||||
@@ -119,6 +171,19 @@ class GeneratedForm extends StatefulWidget {
|
|||||||
State<GeneratedForm> createState() => _GeneratedFormState();
|
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 {
|
class GeneratedFormSubForm extends GeneratedFormItem {
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
|
|
||||||
@@ -129,6 +194,12 @@ class GeneratedFormSubForm extends GeneratedFormItem {
|
|||||||
ensureType(val) {
|
ensureType(val) {
|
||||||
return val; // Not easy to validate List<Map<String, dynamic>>
|
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
|
// Generates a color in the HSLuv (Pastel) color space
|
||||||
@@ -510,14 +581,12 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
]);
|
]);
|
||||||
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
||||||
List<Widget> subformColumn = [];
|
List<Widget> subformColumn = [];
|
||||||
for (int i = 0; i < values[fieldKey].length; i++) {
|
var compact = (widget.items[r][e] as GeneratedFormSubForm)
|
||||||
var items = (widget.items[r][e] as GeneratedFormSubForm)
|
|
||||||
.items
|
.items
|
||||||
.map((x) => x.map((y) {
|
.length ==
|
||||||
y.defaultValue = values[fieldKey]?[i]?[y.key];
|
1 &&
|
||||||
return y;
|
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
|
||||||
}).toList())
|
for (int i = 0; i < values[fieldKey].length; i++) {
|
||||||
.toList();
|
|
||||||
var internalFormKey = ValueKey(generateRandomNumber(
|
var internalFormKey = ValueKey(generateRandomNumber(
|
||||||
values[fieldKey].length,
|
values[fieldKey].length,
|
||||||
seed2: i,
|
seed2: i,
|
||||||
@@ -525,18 +594,28 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
subformColumn.add(Column(
|
subformColumn.add(Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Divider(),
|
if (!compact)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
|
if (!compact)
|
||||||
Text(
|
Text(
|
||||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
key: internalFormKey,
|
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) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
values = values.map(
|
||||||
|
(key, value) => MapEntry(key.split(',')[0], value));
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.values[fieldKey]?[i] = values;
|
this.values[fieldKey]?[i] = values;
|
||||||
}
|
}
|
||||||
@@ -567,13 +646,12 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
Icons.delete_outline_rounded,
|
Icons.delete_outline_rounded,
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
subformColumn.add(Padding(
|
subformColumn.add(Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: const EdgeInsets.only(bottom: 0, top: 8),
|
||||||
bottom: values[fieldKey].length > 0 ? 24 : 0, top: 8),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -591,9 +669,6 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
if (values[fieldKey].length > 0) {
|
|
||||||
subformColumn.add(const Divider());
|
|
||||||
}
|
|
||||||
formInputs[r][e] = Column(children: subformColumn);
|
formInputs[r][e] = Column(children: subformColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,6 @@ import 'package:obtainium/providers/apps_provider.dart';
|
|||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
@@ -19,12 +18,10 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
|||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
import 'package:easy_localization/src/localization.dart';
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.15.6';
|
const String currentVersion = '0.15.10';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
const int bgUpdateCheckAlarmId = 666;
|
|
||||||
|
|
||||||
List<MapEntry<Locale, String>> supportedLocales = const [
|
List<MapEntry<Locale, String>> supportedLocales = const [
|
||||||
MapEntry(Locale('en'), 'English'),
|
MapEntry(Locale('en'), 'English'),
|
||||||
MapEntry(Locale('zh'), '简体中文'),
|
MapEntry(Locale('zh'), '简体中文'),
|
||||||
@@ -176,20 +173,7 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||||
Permission.notification.request();
|
Permission.notification.request();
|
||||||
if (!fdroid) {
|
if (!fdroid) {
|
||||||
appsProvider.saveApps([
|
appsProvider.saveApps([obtainiumApp], onlyIfExists: false);
|
||||||
App(
|
|
||||||
obtainiumId,
|
|
||||||
'https://github.com/ImranR98/Obtainium',
|
|
||||||
'ImranR98',
|
|
||||||
'Obtainium',
|
|
||||||
currentReleaseTag,
|
|
||||||
currentReleaseTag,
|
|
||||||
[],
|
|
||||||
0,
|
|
||||||
{'includePrereleases': true},
|
|
||||||
null,
|
|
||||||
false)
|
|
||||||
], onlyIfExists: false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!supportedLocales
|
if (!supportedLocales
|
||||||
|
@@ -18,7 +18,7 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
Response res = await get(
|
Response res = await get(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
||||||
headers: await GitHub().getRequestHeaders());
|
headers: await GitHub().getRequestHeaders({}));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||||
|
@@ -59,7 +59,9 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
if (updateUrlInput) {
|
if (updateUrlInput) {
|
||||||
urlInputKey++;
|
urlInputKey++;
|
||||||
}
|
}
|
||||||
var prevHost = pickedSource?.host;
|
var prevHost = pickedSource?.hosts.isNotEmpty == true
|
||||||
|
? pickedSource?.hosts[0]
|
||||||
|
: null;
|
||||||
try {
|
try {
|
||||||
var naturalSource =
|
var naturalSource =
|
||||||
valid ? sourceProvider.getSource(userInput) : null;
|
valid ? sourceProvider.getSource(userInput) : null;
|
||||||
@@ -77,7 +79,7 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
overrideSource: pickedSourceOverride)
|
overrideSource: pickedSourceOverride)
|
||||||
: null;
|
: null;
|
||||||
if (pickedSource.runtimeType != source.runtimeType ||
|
if (pickedSource.runtimeType != source.runtimeType ||
|
||||||
(prevHost != null && prevHost != source?.host)) {
|
(prevHost != null && prevHost != source?.hosts[0])) {
|
||||||
pickedSource = source;
|
pickedSource = source;
|
||||||
additionalSettings = source != null
|
additionalSettings = source != null
|
||||||
? getDefaultValuesFromFormItems(
|
? getDefaultValuesFromFormItems(
|
||||||
@@ -508,16 +510,16 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
...sourceProvider.sources.map((e) => GestureDetector(
|
...sourceProvider.sources.map((e) => GestureDetector(
|
||||||
onTap: e.host != null
|
onTap: e.hosts.isNotEmpty
|
||||||
? () {
|
? () {
|
||||||
launchUrlString('https://${e.host}',
|
launchUrlString('https://${e.hosts[0]}',
|
||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
decoration: e.host != null
|
decoration: e.hosts.isNotEmpty
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: TextDecoration.none,
|
: TextDecoration.none,
|
||||||
fontStyle: FontStyle.italic),
|
fontStyle: FontStyle.italic),
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -871,20 +873,44 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
String urls = '';
|
String urls = '';
|
||||||
for (var a in selectedApps) {
|
for (var a in selectedApps) {
|
||||||
urls += 'obtainium://add/${a.url}\n';
|
urls += '${a.url}\n';
|
||||||
}
|
}
|
||||||
urls = urls.substring(0, urls.length - 1);
|
urls = urls.substring(0, urls.length - 1);
|
||||||
Share.share(urls,
|
Share.share(urls,
|
||||||
subject: tr('selectedAppURLsFromObtainium'));
|
subject:
|
||||||
|
'${tr('obtainium')} - ${tr('appsString')}');
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
tooltip: tr('shareSelectedAppURLs'),
|
tooltip: tr('shareSelectedAppURLs'),
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: resetSelectedAppsInstallStatuses,
|
onPressed: selectedAppIds.isEmpty
|
||||||
tooltip: tr('resetInstallStatus'),
|
? null
|
||||||
icon: const Icon(Icons.restore_page_outlined),
|
: () {
|
||||||
|
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="${obtainiumApp.url}">${tr('about')}</a></p>';
|
||||||
|
Share.share(urls,
|
||||||
|
subject:
|
||||||
|
'${tr('obtainium')} - ${tr('appsString')}');
|
||||||
|
},
|
||||||
|
tooltip: tr('shareAppConfigLinks'),
|
||||||
|
icon: const Icon(Icons.ios_share),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@@ -199,10 +199,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
...source.searchQuerySettingFormItems.map((e) => [e]),
|
...source.searchQuerySettingFormItems.map((e) => [e]),
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('url',
|
GeneratedFormTextField('url',
|
||||||
label: source.host != null
|
label: source.hosts.isNotEmpty
|
||||||
? tr('overrideSource')
|
? tr('overrideSource')
|
||||||
: plural('url', 1).substring(2),
|
: plural('url', 1).substring(2),
|
||||||
defaultValue: source.host ?? '',
|
defaultValue:
|
||||||
|
source.hosts.isNotEmpty ? source.hosts[0] : '',
|
||||||
required: true)
|
required: true)
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -212,7 +213,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
if (values['url'] != source.host) {
|
if (values['url'] != source.hosts[0]) {
|
||||||
source = sourceProvider.getSource(values['url'],
|
source = sourceProvider.getSource(values['url'],
|
||||||
overrideSource: source.runtimeType.toString());
|
overrideSource: source.runtimeType.toString());
|
||||||
}
|
}
|
||||||
|
@@ -326,13 +326,15 @@ class AppsProvider with ChangeNotifier {
|
|||||||
AppSource source = SourceProvider()
|
AppSource source = SourceProvider()
|
||||||
.getSource(app.url, overrideSource: app.overrideSource);
|
.getSource(app.url, overrideSource: app.overrideSource);
|
||||||
String downloadUrl = await source.apkUrlPrefetchModifier(
|
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);
|
var notif = DownloadNotification(app.finalName, 100);
|
||||||
notificationsProvider?.cancel(notif.id);
|
notificationsProvider?.cancel(notif.id);
|
||||||
int? prevProg;
|
int? prevProg;
|
||||||
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
||||||
var headers = await source.getRequestHeaders(
|
var headers = await source.getRequestHeaders(app.additionalSettings,
|
||||||
additionalSettings: app.additionalSettings, forAPKDownload: true);
|
forAPKDownload: true);
|
||||||
var downloadedFile = await downloadFileWithRetry(
|
var downloadedFile = await downloadFileWithRetry(
|
||||||
downloadUrl, fileNameNoExt,
|
downloadUrl, fileNameNoExt,
|
||||||
headers: headers, (double? progress) {
|
headers: headers, (double? progress) {
|
||||||
@@ -402,7 +404,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
Future<bool> canInstallSilently(App app) async {
|
Future<bool> canInstallSilently(App app) async {
|
||||||
if (app.id == obtainiumId) {
|
if (app.id == obtainiumApp.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!settingsProvider.enableBackgroundUpdates) {
|
if (!settingsProvider.enableBackgroundUpdates) {
|
||||||
@@ -426,7 +428,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Probably not installed - ignore
|
// Probably not installed - ignore
|
||||||
}
|
}
|
||||||
if (installerPackageName != obtainiumId) {
|
if (installerPackageName != obtainiumApp.id) {
|
||||||
// If we did not install the app (or it isn't installed), silent install is not possible
|
// If we did not install the app (or it isn't installed), silent install is not possible
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -671,7 +673,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Move Obtainium to the end of the line (let all other apps update first)
|
// Move Obtainium to the end of the line (let all other apps update first)
|
||||||
appsToInstall =
|
appsToInstall =
|
||||||
moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
|
moveStrToEnd(appsToInstall, obtainiumApp.id, strB: obtainiumTempId);
|
||||||
|
|
||||||
Future<void> updateFn(String id, {bool skipInstalls = false}) async {
|
Future<void> updateFn(String id, {bool skipInstalls = false}) async {
|
||||||
try {
|
try {
|
||||||
@@ -796,13 +798,17 @@ class AppsProvider with ChangeNotifier {
|
|||||||
SourceProvider()
|
SourceProvider()
|
||||||
.getSource(app.app.url, overrideSource: app.app.overrideSource)
|
.getSource(app.app.url, overrideSource: app.app.overrideSource)
|
||||||
.naiveStandardVersionDetection;
|
.naiveStandardVersionDetection;
|
||||||
|
String? realInstalledVersion =
|
||||||
|
app.app.additionalSettings['useVersionCodeAsOSVersion'] == true
|
||||||
|
? app.installedInfo?.versionCode.toString()
|
||||||
|
: app.installedInfo?.versionName;
|
||||||
return app.app.additionalSettings['trackOnly'] != true &&
|
return app.app.additionalSettings['trackOnly'] != true &&
|
||||||
app.app.additionalSettings['versionDetection'] !=
|
app.app.additionalSettings['versionDetection'] !=
|
||||||
'releaseDateAsVersion' &&
|
'releaseDateAsVersion' &&
|
||||||
app.installedInfo?.versionName != null &&
|
realInstalledVersion != null &&
|
||||||
app.app.installedVersion != null &&
|
app.app.installedVersion != null &&
|
||||||
(reconcileVersionDifferences(app.installedInfo!.versionName!,
|
(reconcileVersionDifferences(
|
||||||
app.app.installedVersion!) !=
|
realInstalledVersion, app.app.installedVersion!) !=
|
||||||
null ||
|
null ||
|
||||||
naiveStandardVersionDetection);
|
naiveStandardVersionDetection);
|
||||||
}
|
}
|
||||||
@@ -821,30 +827,33 @@ class AppsProvider with ChangeNotifier {
|
|||||||
SourceProvider()
|
SourceProvider()
|
||||||
.getSource(app.url, overrideSource: app.overrideSource)
|
.getSource(app.url, overrideSource: app.overrideSource)
|
||||||
.naiveStandardVersionDetection;
|
.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
|
// FIRST, COMPARE THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE ONE IS NULL
|
||||||
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
|
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 says it's installed but isn't really (and isn't track only) - set to not installed
|
||||||
app.installedVersion = null;
|
app.installedVersion = null;
|
||||||
modded = true;
|
modded = true;
|
||||||
} else if (installedInfo?.versionName != null &&
|
} else if (realInstalledVersion != null && app.installedVersion == 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 says it's not installed but really is - set to installed and use real package versionName
|
app.installedVersion = realInstalledVersion;
|
||||||
app.installedVersion = installedInfo!.versionName;
|
|
||||||
modded = true;
|
modded = true;
|
||||||
}
|
}
|
||||||
// SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL
|
// SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL
|
||||||
if (installedInfo?.versionName != null &&
|
if (realInstalledVersion != null &&
|
||||||
installedInfo!.versionName != app.installedVersion &&
|
realInstalledVersion != app.installedVersion &&
|
||||||
versionDetectionIsStandard) {
|
versionDetectionIsStandard) {
|
||||||
// App's reported version and real version don't match (and it uses standard version detection)
|
// 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
|
// If they share a standard format (and are still different under it), update the reported version accordingly
|
||||||
var correctedInstalledVersion = reconcileVersionDifferences(
|
var correctedInstalledVersion = reconcileVersionDifferences(
|
||||||
installedInfo.versionName!, app.installedVersion!);
|
realInstalledVersion, app.installedVersion!);
|
||||||
if (correctedInstalledVersion?.key == false) {
|
if (correctedInstalledVersion?.key == false) {
|
||||||
app.installedVersion = correctedInstalledVersion!.value;
|
app.installedVersion = correctedInstalledVersion!.value;
|
||||||
modded = true;
|
modded = true;
|
||||||
} else if (naiveStandardVersionDetection) {
|
} else if (naiveStandardVersionDetection) {
|
||||||
app.installedVersion = installedInfo.versionName;
|
app.installedVersion = realInstalledVersion;
|
||||||
modded = true;
|
modded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1289,8 +1298,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
await Future.delayed(const Duration(microseconds: 1));
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
}
|
}
|
||||||
for (App a in importedApps) {
|
for (App a in importedApps) {
|
||||||
|
var installedInfo = await getInstalledInfo(a.id, printErr: false);
|
||||||
a.installedVersion =
|
a.installedVersion =
|
||||||
(await getInstalledInfo(a.id, printErr: false))?.versionName;
|
a.additionalSettings['useVersionCodeAsOSVersion'] == true
|
||||||
|
? installedInfo?.versionCode.toString()
|
||||||
|
: installedInfo?.versionName;
|
||||||
}
|
}
|
||||||
await saveApps(importedApps, onlyIfExists: false);
|
await saveApps(importedApps, onlyIfExists: false);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1664,7 +1676,8 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
|
|||||||
}
|
}
|
||||||
if (toInstall.isNotEmpty) {
|
if (toInstall.isNotEmpty) {
|
||||||
logs.add('BG install task: Started (${toInstall.length}).');
|
logs.add('BG install task: Started (${toInstall.length}).');
|
||||||
var tempObtArr = toInstall.where((element) => element.key == obtainiumId);
|
var tempObtArr =
|
||||||
|
toInstall.where((element) => element.key == obtainiumApp.id);
|
||||||
if (tempObtArr.isNotEmpty) {
|
if (tempObtArr.isNotEmpty) {
|
||||||
// Move obtainium to the end of the list as it must always install last
|
// Move obtainium to the end of the list as it must always install last
|
||||||
var obt = tempObtArr.first;
|
var obt = tempObtArr.first;
|
||||||
|
@@ -14,8 +14,23 @@ import 'package:permission_handler/permission_handler.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:shared_storage/shared_storage.dart' as saf;
|
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';
|
|
||||||
|
App obtainiumApp = App(
|
||||||
|
'dev.imranr.obtainium',
|
||||||
|
'https://github.com/ImranR98/Obtainium',
|
||||||
|
'ImranR98',
|
||||||
|
'Obtainium',
|
||||||
|
currentReleaseTag,
|
||||||
|
currentReleaseTag,
|
||||||
|
[],
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
'includePrereleases': true,
|
||||||
|
'versionDetection': 'standardVersionDetection'
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
enum InstallMethodSettings { normal, shizuku, root }
|
enum InstallMethodSettings { normal, shizuku, root }
|
||||||
|
|
||||||
|
@@ -19,7 +19,6 @@ import 'package:obtainium/app_sources/huaweiappgallery.dart';
|
|||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
import 'package:obtainium/app_sources/jenkins.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/neutroncode.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
@@ -277,9 +276,10 @@ class App {
|
|||||||
json['installedVersion'] == null
|
json['installedVersion'] == null
|
||||||
? null
|
? null
|
||||||
: json['installedVersion'] as String,
|
: json['installedVersion'] as String,
|
||||||
json['latestVersion'] as String,
|
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||||
assumed2DlistToStringMapList(jsonDecode(json['apkUrls'])),
|
assumed2DlistToStringMapList(jsonDecode(
|
||||||
json['preferredApkIndex'] as int,
|
(json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||||
|
(json['preferredApkIndex'] ?? -1) as int,
|
||||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
? null
|
? null
|
||||||
@@ -366,8 +366,12 @@ List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
|
|||||||
return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
|
return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
getSourceRegex(List<String> hosts) {
|
||||||
|
return '(${hosts.join('|').replaceAll('.', '\\.')})';
|
||||||
|
}
|
||||||
|
|
||||||
abstract class AppSource {
|
abstract class AppSource {
|
||||||
String? host;
|
List<String> hosts = [];
|
||||||
bool hostChanged = false;
|
bool hostChanged = false;
|
||||||
late String name;
|
late String name;
|
||||||
bool enforceTrackOnly = false;
|
bool enforceTrackOnly = false;
|
||||||
@@ -413,8 +417,8 @@ abstract class AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
Map<String, dynamic> additionalSettings,
|
||||||
bool forAPKDownload = false}) async {
|
{bool forAPKDownload = false}) async {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,12 +426,10 @@ abstract class AppSource {
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> sourceRequest(String url,
|
Future<Response> sourceRequest(
|
||||||
{bool followRedirects = true,
|
String url, Map<String, dynamic> additionalSettings,
|
||||||
Map<String, dynamic> additionalSettings =
|
{bool followRedirects = true}) async {
|
||||||
const <String, dynamic>{}}) async {
|
var requestHeaders = await getRequestHeaders(additionalSettings);
|
||||||
var requestHeaders =
|
|
||||||
await getRequestHeaders(additionalSettings: additionalSettings);
|
|
||||||
if (requestHeaders != null || followRedirects == false) {
|
if (requestHeaders != null || followRedirects == false) {
|
||||||
var req = Request('GET', Uri.parse(url));
|
var req = Request('GET', Uri.parse(url));
|
||||||
req.followRedirects = followRedirects;
|
req.followRedirects = followRedirects;
|
||||||
@@ -484,6 +486,10 @@ abstract class AppSource {
|
|||||||
label: tr('versionDetection'),
|
label: tr('versionDetection'),
|
||||||
defaultValue: 'standardVersionDetection')
|
defaultValue: 'standardVersionDetection')
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('useVersionCodeAsOSVersion',
|
||||||
|
label: tr('useVersionCodeAsOSVersion'), defaultValue: false)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('apkFilterRegEx',
|
GeneratedFormTextField('apkFilterRegEx',
|
||||||
label: tr('filterAPKsByRegEx'),
|
label: tr('filterAPKsByRegEx'),
|
||||||
@@ -544,8 +550,8 @@ abstract class AppSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> apkUrlPrefetchModifier(
|
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||||
String apkUrl, String standardUrl) async {
|
Map<String, dynamic> additionalSettings) async {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +682,6 @@ class SourceProvider {
|
|||||||
APKMirror(),
|
APKMirror(),
|
||||||
HuaweiAppGallery(),
|
HuaweiAppGallery(),
|
||||||
Jenkins(),
|
Jenkins(),
|
||||||
Mullvad(),
|
|
||||||
Signal(),
|
Signal(),
|
||||||
VLC(),
|
VLC(),
|
||||||
WhatsApp(),
|
WhatsApp(),
|
||||||
@@ -697,14 +702,14 @@ class SourceProvider {
|
|||||||
throw UnsupportedURLError();
|
throw UnsupportedURLError();
|
||||||
}
|
}
|
||||||
var res = srcs.first;
|
var res = srcs.first;
|
||||||
res.host = Uri.parse(url).host;
|
res.hosts = [Uri.parse(url).host];
|
||||||
res.hostChanged = true;
|
res.hostChanged = true;
|
||||||
return srcs.first;
|
return srcs.first;
|
||||||
}
|
}
|
||||||
AppSource? source;
|
AppSource? source;
|
||||||
for (var s in sources.where((element) => element.host != null)) {
|
for (var s in sources.where((element) => element.hosts.isNotEmpty)) {
|
||||||
if (RegExp(
|
if (RegExp(
|
||||||
'://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?')
|
'://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})(/|\\z)?')
|
||||||
.hasMatch(url)) {
|
.hasMatch(url)) {
|
||||||
source = s;
|
source = s;
|
||||||
break;
|
break;
|
||||||
@@ -712,7 +717,7 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
for (var s in sources.where(
|
for (var s in sources.where(
|
||||||
(element) => element.host == null && !element.neverAutoSelect)) {
|
(element) => element.hosts.isEmpty && !element.neverAutoSelect)) {
|
||||||
try {
|
try {
|
||||||
s.sourceSpecificStandardizeURL(url);
|
s.sourceSpecificStandardizeURL(url);
|
||||||
source = s;
|
source = s;
|
||||||
|
72
pubspec.lock
72
pubspec.lock
@@ -22,10 +22,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: android_package_manager
|
name: android_package_manager
|
||||||
sha256: b873fe5856f7c442aca9751dac05d117285be9e4de08eb15d1ffb811fd1b688d
|
sha256: e52ca607b9f19f95d5dae4211ed8fa93e67093f22ac570db47489c5bca512940
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.7.0"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -70,10 +70,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: background_fetch
|
name: background_fetch
|
||||||
sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4
|
sha256: "34550cf9b383e5a1844e7d22119aa500508c7df9421fa967c9fb4430d6cb2878"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -258,6 +258,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.1"
|
version: "6.1.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -402,10 +410,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
|
sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.3"
|
version: "4.1.4"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -506,10 +514,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -522,10 +530,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.2"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -538,10 +546,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -674,10 +682,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
|
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.3.5"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -690,10 +698,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
|
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.2"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -823,26 +831,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
|
sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.2"
|
version: "6.2.3"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51
|
sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.1"
|
version: "6.2.2"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea"
|
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.2"
|
version: "6.2.4"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -863,10 +871,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_platform_interface
|
name: url_launcher_platform_interface
|
||||||
sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198"
|
sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.1"
|
||||||
url_launcher_web:
|
url_launcher_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -887,10 +895,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
|
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.2"
|
version: "4.3.3"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -911,10 +919,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter
|
name: webview_flutter
|
||||||
sha256: "60e23976834e995c404c0b21d3b9db37ecd77d3303ef74f8b8d7a7b19947fc04"
|
sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.3"
|
version: "4.4.4"
|
||||||
webview_flutter_android:
|
webview_flutter_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -927,18 +935,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
|
sha256: "80b40ae4fb959957eef9fa8970b6c9accda9f49fc45c2b75154696a8e8996cfe"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.0"
|
version: "2.9.1"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: "02d8f3ebbc842704b2b662377b3ee11c0f8f1bbaa8eab6398262f40049819160"
|
sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.10.1"
|
version: "3.10.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.15.6+242 # When changing this, update the tag in main() accordingly
|
version: 0.15.10+246 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -55,7 +55,7 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/ImranR98/android_package_installer
|
url: https://github.com/ImranR98/android_package_installer
|
||||||
ref: main
|
ref: main
|
||||||
android_package_manager: ^0.6.0
|
android_package_manager: ^0.7.0
|
||||||
share_plus: ^7.0.0
|
share_plus: ^7.0.0
|
||||||
sqflite: ^2.2.0+3
|
sqflite: ^2.2.0+3
|
||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
|
Reference in New Issue
Block a user