Compare commits

...

66 Commits

Author SHA1 Message Date
Imran Remtulla
dc52fb6181 Merge pull request #473 from ImranR98/dev
Sourceforge apk url extraction bugfix
2023-04-15 15:19:08 -04:00
Imran Remtulla
9e4ac397d8 Sourceforge apk url extraction bugfix 2023-04-15 15:18:32 -04:00
Imran Remtulla
0ec944eae9 Merge pull request #472 from ImranR98/dev
Bugfix in getting APK name from URL (affected Sourceforge and potentially others)
2023-04-15 14:43:02 -04:00
Imran Remtulla
ad250c30e4 Increment version 2023-04-15 14:41:38 -04:00
Imran Remtulla
1090f15508 Sourceforge bugfix 2023-04-15 14:34:14 -04:00
Imran Remtulla
666941350e APK name bugfix 2023-04-15 14:28:00 -04:00
Imran Remtulla
eeadbce8b0 Merge pull request #466 from ImranR98/dev
Increment version, update packages
2023-04-13 23:06:22 -04:00
Imran Remtulla
ce8aeff342 Increment version, update packages 2023-04-13 23:06:08 -04:00
Imran Remtulla
0d8362a2ed Merge pull request #465 from Bnyro/amoled-theme
Add an amoled black theme
2023-04-13 22:59:54 -04:00
Bnyro
3b28143a4e Add an amoled black theme 2023-04-13 18:19:24 +02:00
Imran Remtulla
537628f378 Merge pull request #451 from gidano/gidano/Obtainium-HU
Updated hu.json
2023-04-12 15:49:02 -04:00
Imran Remtulla
c92d76df98 Merge pull request #453 from mehdijahann/main
Update fa.json
2023-04-12 15:48:51 -04:00
Imran Remtulla
b6959e1a8b Merge pull request #457 from markus-gitdev/main
Update de.json
2023-04-12 15:48:42 -04:00
Imran Remtulla
1bf648da60 Merge pull request #461 from ImranR98/dev
Fixed HTML relative link handling (#455), Fixed App name override and sort inconsistencies (#450)
2023-04-12 15:48:28 -04:00
Imran Remtulla
6a1275e9e4 Sort no longer case-sensitive (#450) 2023-04-12 15:46:48 -04:00
Imran Remtulla
df242b91ad Increment version, update packages 2023-04-12 15:39:32 -04:00
Imran Remtulla
7ea75325bb App name overrides more consistent (#450) 2023-04-12 15:36:17 -04:00
Imran Remtulla
0704dfe2ee Fixed relative link handling in HTML source 2023-04-12 15:17:08 -04:00
Imran Remtulla
6275cbf114 HTML Source - handle relative URLs in literal .html pages 2023-04-12 14:50:54 -04:00
Markus
36b8ef6782 Update de.json
Translations for:
- groupByCategory
- autoApkFilterByArch
2023-04-11 13:10:03 +02:00
Mehdee
d274b9a428 Update fa.json 2023-04-10 16:54:29 +02:00
gidano
1c2980d1ac Updated hu.json 2023-04-09 08:57:12 +02:00
Imran Remtulla
8f0aac057e Merge pull request #442 from bluefly000/japanese-translation
Update ja.json
2023-04-07 22:11:35 -04:00
Imran Remtulla
e929920a48 Merge pull request #440 from atilluF/main
Update it.json
2023-04-07 22:11:27 -04:00
Imran Remtulla
8ed254c7dd Merge pull request #446 from ImranR98/dev
Bugfix: GitHub/Codeberg fallback + no-prerel fail
2023-04-07 22:11:20 -04:00
Imran Remtulla
46a00836df Bugfix: GitHub/Codeberg fallback + no-prerel fail 2023-04-07 22:10:55 -04:00
bluefly000
f144ffdded Update ja.json 2023-04-08 00:03:31 +09:00
atilluF
d597d569e2 Update it.json 2023-04-07 13:19:12 +02:00
Imran Remtulla
b62475de87 Merge pull request #439 from ImranR98/dev
Use app deep copy in places to avoid bugs
2023-04-07 02:20:32 -04:00
Imran Remtulla
334ac8d3d6 Use app deep copy in places to avoid bugs 2023-04-07 01:54:14 -04:00
Imran Remtulla
9193788356 Merge pull request #438 from ImranR98/dev
Better downloaded file naming (reduces conflicts)
2023-04-06 23:00:28 -04:00
Imran Remtulla
8f75ddd43f Better download file naming (reduces conflicts) 2023-04-06 22:59:40 -04:00
Imran Remtulla
a2edc86bfa Merge pull request #437 from ImranR98/dev
Added simple APK try auto-select by CPU arch #436
2023-04-06 22:37:55 -04:00
Imran Remtulla
0804e680b2 Added simple APK try auto-select by CPU arch #436
Plus minor form switch UI fixes (overflow, spacing)
2023-04-06 22:37:24 -04:00
Imran Remtulla
49affd1bd4 Merge pull request #434 from ImranR98/dev
Store APK names with URLs (#432)
2023-04-05 18:56:16 -04:00
Imran Remtulla
202ce4f0d5 Store APK names with URLs (#432) 2023-04-05 18:50:19 -04:00
Imran Remtulla
361a3e1bc2 Merge pull request #426 from ImranR98/dev
Increment version
2023-04-04 21:46:15 -04:00
Imran Remtulla
f33a26d4f4 Increment version 2023-04-04 21:45:57 -04:00
Imran Remtulla
7aaf56ec8c Merge pull request #425 from HRTK92/main
Add long-press URL copy and snackbar message
2023-04-04 21:44:35 -04:00
HRTK92
ed120016d9 Add long-press URL copy and snackbar message 2023-04-05 10:32:56 +09:00
Imran Remtulla
e8cbac8657 Merge pull request #413 from gidano/Obtainium-HU
Done
2023-04-04 21:13:23 -04:00
Imran Remtulla
b66c13d319 Merge pull request #424 from ImranR98/dev
Bugfix #392, Custom App Names #420, Archive Label in GitHub Search #421
2023-04-04 21:05:54 -04:00
Imran Remtulla
782d055bc3 Added cloudflare.f-droid.org support 2023-04-04 20:21:24 -04:00
Imran Remtulla
d557746965 Increment version 2023-04-04 20:00:36 -04:00
Imran Remtulla
e6b05d50b9 Scrolling bugfix #392, custom name #420, search archive label #421 2023-04-04 19:59:35 -04:00
Imran Remtulla
dea635fa6a Merge pull request #416 from ImranR98/dev
Added a source filter to the Apps page
2023-04-02 18:14:59 -04:00
Imran Remtulla
682026ed0a Added a source filter to the Apps page 2023-04-02 18:14:43 -04:00
gidano
9fe8a200ef Done 2023-04-01 14:55:56 +02:00
Imran Remtulla
210100da2b Merge pull request #412 from ImranR98/dev
Attempt to workaround export bug (#385)
2023-04-01 00:43:11 -04:00
Imran Remtulla
d52660235b Attempt to workaround export bug (#385) 2023-04-01 00:42:44 -04:00
Imran Remtulla
e386b5ab8a Merge pull request #411 from ImranR98/dev
Bugfix: App pinning not working (#410)
2023-04-01 00:24:56 -04:00
Imran Remtulla
abf7be222d Bugfix: App pinning not working (#410) 2023-04-01 00:24:16 -04:00
Imran Remtulla
4c5b9304c0 Merge pull request #409 from ImranR98/dev
Bugfix for prev. commit
2023-03-31 15:40:18 -04:00
Imran Remtulla
4cfe6af044 Bugfix for prev. commit 2023-03-31 15:39:52 -04:00
Imran Remtulla
3f0c4068dd Merge pull request #408 from ImranR98/dev
Bugfix #405 + general categories bugfixes
2023-03-31 15:37:11 -04:00
Imran Remtulla
7981ca29c5 Bugfix #405 + general categories bugfixes 2023-03-31 15:36:51 -04:00
Imran Remtulla
187efa8fc5 Merge pull request #406 from ImranR98/dev
Fixed Mullvad web scraping (again)
2023-03-31 09:25:50 -04:00
Imran Remtulla
cd27ff7f2d Fixed Mullvad web scraping (again) 2023-03-31 09:24:15 -04:00
Imran Remtulla
6f6a25511b Merge pull request #402 from ImranR98/dev
Added "Group by Category" setting
2023-03-30 23:41:06 -04:00
Imran Remtulla
4e17bbcfd1 Added "Group by Category" setting 2023-03-30 23:40:32 -04:00
Imran Remtulla
814e269d1d Merge pull request #401 from ImranR98/dev
Bugfix: "releaseDateAsVersion" resets to "noVersionDetection"
2023-03-30 17:45:24 -04:00
Imran Remtulla
6b7d962b87 Bugfix: "releaseDateAsVersion" resets to "noVersionDetection"
Also 2 related UI fixes
2023-03-30 17:44:39 -04:00
Imran Remtulla
9fba747802 Version detection improvements, Mullvad web scraping fix and changelog addition, code readability improvements, general tweaks/bugfixes (#400)
1. Apps that don't have "standard" versioning formats now automatically stop using version detection. This will prevent users from having to learn about this feature and enable it manually.
    - For such Apps, the "standard" version detection option is greyed out.
2. The Mullvad Source recently broke due to a slight change in their website design. This is now fixed.
    - Mullvad also now provides an in-app changelog via their official GitHub repo.
3. Code has been refactored for readability (specifically the version detection code and UI code for most screens).
4. Minor UI tweaks and bugfixes.
2023-03-30 17:27:36 -04:00
Imran Remtulla
c7cd35b6a1 Merge pull request #388 from ImranR98/dev
Hide (non-functional) uninstall button for track-only Apps, attempt bugfix for cert error (#375)
2023-03-26 07:39:34 -04:00
Imran Remtulla
a8a3fce33a Attempt to fix bug (#375) 2023-03-26 07:37:35 -04:00
Imran Remtulla
3a38cedcf5 Bugfix: Don't show uninstall option for track-only apps 2023-03-25 22:55:40 -04:00
36 changed files with 2263 additions and 2029 deletions

View File

@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
nLRbwHOoq7hHwg==
-----END CERTIFICATE-----

View File

@@ -122,6 +122,7 @@
"followSystem": "System folgen", "followSystem": "System folgen",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "App sortieren nach", "appSortBy": "App sortieren nach",
"authorName": "Autor/Name", "authorName": "Autor/Name",
"nameAuthor": "Name/Autor", "nameAuthor": "Name/Autor",
@@ -207,6 +208,7 @@
"addCategory": "Kategorie hinzufügen", "addCategory": "Kategorie hinzufügen",
"label": "Bezeichnung", "label": "Bezeichnung",
"language": "Sprache", "language": "Sprache",
"copiedToClipboard": "Copied to Clipboard",
"storagePermissionDenied": "Speicherberechtigung verweigert", "storagePermissionDenied": "Speicherberechtigung verweigert",
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.", "selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern", "filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)", "importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)",
"versionDetection": "Versionserkennung", "versionDetection": "Versionserkennung",
"standardVersionDetection": "Standardversionserkennung", "standardVersionDetection": "Standardversionserkennung",
"groupByCategory": "Nach Kategorie gruppieren",
"autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
"removeAppQuestion": { "removeAppQuestion": {
"one": "App entfernen?", "one": "App entfernen?",
"other": "App entfernen?" "other": "App entfernen?"
@@ -268,4 +272,4 @@
"one": "{} und 1 weitere Anwendung wurden aktualisiert.", "one": "{} und 1 weitere Anwendung wurden aktualisiert.",
"other": "{} und {} weitere Anwendungen wurden aktualisiert." "other": "{} und {} weitere Anwendungen wurden aktualisiert."
} }
} }

View File

@@ -122,6 +122,7 @@
"followSystem": "Follow System", "followSystem": "Follow System",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "App Sort By", "appSortBy": "App Sort By",
"authorName": "Author/Name", "authorName": "Author/Name",
"nameAuthor": "Name/Author", "nameAuthor": "Name/Author",
@@ -207,6 +208,7 @@
"addCategory": "Add Category", "addCategory": "Add Category",
"label": "Label", "label": "Label",
"language": "Language", "language": "Language",
"copiedToClipboard": "Copied to Clipboard",
"storagePermissionDenied": "Storage permission denied", "storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"filterAPKsByRegEx": "Filter APKs by Regular Expression", "filterAPKsByRegEx": "Filter APKs by Regular Expression",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "Import from URLs in File (like OPML)", "importFromURLsInFile": "Import from URLs in File (like OPML)",
"versionDetection": "Version Detection", "versionDetection": "Version Detection",
"standardVersionDetection": "Standard version detection", "standardVersionDetection": "Standard version detection",
"groupByCategory": "Group by Category",
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remove App?", "one": "Remove App?",
"other": "Remove Apps?" "other": "Remove Apps?"
@@ -268,4 +272,4 @@
"one": "{} and 1 more app were updated.", "one": "{} and 1 more app were updated.",
"other": "{} and {} more apps were updated." "other": "{} and {} more apps were updated."
} }
} }

View File

@@ -122,6 +122,7 @@
"followSystem": "هماهنگ با سیستم", "followSystem": "هماهنگ با سیستم",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "مرتب سازی برنامه بر اساس", "appSortBy": "مرتب سازی برنامه بر اساس",
"authorName": "سازنده/اسم", "authorName": "سازنده/اسم",
"nameAuthor": "اسم/سازنده", "nameAuthor": "اسم/سازنده",
@@ -207,6 +208,7 @@
"addCategory": "اضافه کردن دسته", "addCategory": "اضافه کردن دسته",
"label": "برچسب", "label": "برچسب",
"language": "زبان", "language": "زبان",
"copiedToClipboard": "در کلیپ بورد کپی شد",
"storagePermissionDenied": "مجوز ذخیره سازی رد شد", "storagePermissionDenied": "مجوز ذخیره سازی رد شد",
"selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.", "selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.",
"filterAPKsByRegEx": "فایل‌های APK را با نظم فیلتر کنید", "filterAPKsByRegEx": "فایل‌های APK را با نظم فیلتر کنید",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)", "importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
"versionDetection": "تشخیص نسخه", "versionDetection": "تشخیص نسخه",
"standardVersionDetection": "تشخیص نسخه استاندارد", "standardVersionDetection": "تشخیص نسخه استاندارد",
"groupByCategory": "گروه بر اساس دسته",
"autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
"removeAppQuestion": { "removeAppQuestion": {
"one": "برنامه حذف شود؟", "one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟" "other": "برنامه ها حذف شوند؟"

View File

@@ -122,6 +122,7 @@
"followSystem": "Suivre le système", "followSystem": "Suivre le système",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "Applications triées par", "appSortBy": "Applications triées par",
"authorName": "Auteur/Nom", "authorName": "Auteur/Nom",
"nameAuthor": "Nom/Auteur", "nameAuthor": "Nom/Auteur",
@@ -207,6 +208,7 @@
"addCategory": "Ajouter une catégorie", "addCategory": "Ajouter une catégorie",
"label": "Étiquette", "label": "Étiquette",
"language": "Langue", "language": "Langue",
"copiedToClipboard": "Copied to Clipboard",
"storagePermissionDenied": "Autorisation de stockage refusée", "storagePermissionDenied": "Autorisation de stockage refusée",
"selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.", "selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.",
"filterAPKsByRegEx": "Filtrer les APK par expression régulière", "filterAPKsByRegEx": "Filtrer les APK par expression régulière",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "Importer à partir d'URL dans un fichier (comme OPML)", "importFromURLsInFile": "Importer à partir d'URL dans un fichier (comme OPML)",
"versionDetection": "Détection des versions", "versionDetection": "Détection des versions",
"standardVersionDetection": "Détection de version standard", "standardVersionDetection": "Détection de version standard",
"groupByCategory": "Group by Category",
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Supprimer l'application ?", "one": "Supprimer l'application ?",
"other": "Supprimer les applications ?" "other": "Supprimer les applications ?"

View File

@@ -122,6 +122,7 @@
"followSystem": "Rendszer szerint", "followSystem": "Rendszer szerint",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "App rendezés...", "appSortBy": "App rendezés...",
"authorName": "Szerző/Név", "authorName": "Szerző/Név",
"nameAuthor": "Név/Szerző", "nameAuthor": "Név/Szerző",
@@ -206,6 +207,7 @@
"addCategory": "Új kategória", "addCategory": "Új kategória",
"label": "Címke", "label": "Címke",
"language": "Nyelv", "language": "Nyelv",
"copiedToClipboard": "Copied to Clipboard",
"storagePermissionDenied": "Tárhely engedély megtagadva", "storagePermissionDenied": "Tárhely engedély megtagadva",
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel", "filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
@@ -219,6 +221,8 @@
"importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)", "importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
"versionDetection": "Verzió érzékelés", "versionDetection": "Verzió érzékelés",
"standardVersionDetection": "Alapért. verzió érzékelés", "standardVersionDetection": "Alapért. verzió érzékelés",
"groupByCategory": "Csoportosítás Kategória alapján",
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
"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?"

View File

@@ -122,6 +122,7 @@
"followSystem": "Segui sistema", "followSystem": "Segui sistema",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "App ordinate per", "appSortBy": "App ordinate per",
"authorName": "Autore/Nome", "authorName": "Autore/Nome",
"nameAuthor": "Nome/Autore", "nameAuthor": "Nome/Autore",
@@ -207,6 +208,7 @@
"addCategory": "Aggiungi categoria", "addCategory": "Aggiungi categoria",
"label": "Etichetta", "label": "Etichetta",
"language": "Lingua", "language": "Lingua",
"copiedToClipboard": "Copiato negli appunti",
"storagePermissionDenied": "Accesso ai file non autorizzato", "storagePermissionDenied": "Accesso ai file non autorizzato",
"selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.", "selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.",
"filterAPKsByRegEx": "Filtra file APK con espressioni regolari", "filterAPKsByRegEx": "Filtra file APK con espressioni regolari",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "Importa da URL in file (come OPML)", "importFromURLsInFile": "Importa da URL in file (come OPML)",
"versionDetection": "Rilevamento di versione", "versionDetection": "Rilevamento di versione",
"standardVersionDetection": "Rilevamento di versione standard", "standardVersionDetection": "Rilevamento di versione standard",
"groupByCategory": "Raggruppa per categoria",
"autoApkFilterByArch": "Tenta di filtrare gli APK in base all'architettura della CPU, se possibile",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Rimuovere l'App?", "one": "Rimuovere l'App?",
"other": "Rimuovere le App?" "other": "Rimuovere le App?"

View File

@@ -122,6 +122,7 @@
"followSystem": "システムに従う", "followSystem": "システムに従う",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "アプリの並び方", "appSortBy": "アプリの並び方",
"authorName": "作者名/アプリ名", "authorName": "作者名/アプリ名",
"nameAuthor": "アプリ名/作者名", "nameAuthor": "アプリ名/作者名",
@@ -207,6 +208,7 @@
"addCategory": "カテゴリを追加", "addCategory": "カテゴリを追加",
"label": "ラベル", "label": "ラベル",
"language": "言語", "language": "言語",
"copiedToClipboard": "クリップボードにコピーしました",
"storagePermissionDenied": "ストレージ権限が拒否されました", "storagePermissionDenied": "ストレージ権限が拒否されました",
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
"filterAPKsByRegEx": "正規表現でAPKを絞り込む", "filterAPKsByRegEx": "正規表現でAPKを絞り込む",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "ファイルOPMLなど内のURLからインポート", "importFromURLsInFile": "ファイルOPMLなど内のURLからインポート",
"versionDetection": "バージョン検出", "versionDetection": "バージョン検出",
"standardVersionDetection": "標準のバージョン検出", "standardVersionDetection": "標準のバージョン検出",
"groupByCategory": "カテゴリ別にグループ化する",
"autoApkFilterByArch": "可能であればCPUアーキテクチャによるAPKのフィルタリングを試みる",
"removeAppQuestion": { "removeAppQuestion": {
"one": "アプリを削除しますか?", "one": "アプリを削除しますか?",
"other": "アプリを削除しますか?" "other": "アプリを削除しますか?"
@@ -268,4 +272,4 @@
"one": "{} とさらに {} 個のアプリがアップデートされました", "one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました" "other": "{} とさらに {} 個のアプリがアップデートされました"
} }
} }

View File

@@ -123,6 +123,7 @@
"followSystem": "跟随系统", "followSystem": "跟随系统",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"appSortBy": "排列方式", "appSortBy": "排列方式",
"authorName": "作者 / 名字", "authorName": "作者 / 名字",
"nameAuthor": "名字 / 作者", "nameAuthor": "名字 / 作者",
@@ -208,6 +209,7 @@
"addCategory": "添加类别", "addCategory": "添加类别",
"label": "标签", "label": "标签",
"language": "语言", "language": "语言",
"copiedToClipboard": "Copied to Clipboard",
"storagePermissionDenied": "存储权限已被拒绝", "storagePermissionDenied": "存储权限已被拒绝",
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别", "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
"filterAPKsByRegEx": "Filter APKs by Regular Expression", "filterAPKsByRegEx": "Filter APKs by Regular Expression",
@@ -220,6 +222,8 @@
"importFromURLsInFile": "Import from URLs in File (like OPML)", "importFromURLsInFile": "Import from URLs in File (like OPML)",
"versionDetection": "Version Detection", "versionDetection": "Version Detection",
"standardVersionDetection": "Standard version detection", "standardVersionDetection": "Standard version detection",
"groupByCategory": "Group by Category",
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
"removeAppQuestion": { "removeAppQuestion": {
"one": "删除应用?", "one": "删除应用?",
"other": "删除应用?" "other": "删除应用?"
@@ -268,4 +272,4 @@
"one": "{} 和 {} 更多应用已被安装", "one": "{} 和 {} 更多应用已被安装",
"other": "{} 和 {} 更多应用已被安装" "other": "{} 和 {} 更多应用已被安装"
} }
} }

View File

@@ -68,7 +68,7 @@ class Codeberg extends AppSource {
if (res.statusCode == 200) { if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>; var releases = jsonDecode(res.body) as List<dynamic>;
List<String> getReleaseAPKUrls(dynamic release) => List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?) (release['assets'] as List<dynamic>?)
?.map((e) { ?.map((e) {
return e['name'] != null && e['browser_download_url'] != null return e['name'] != null && e['browser_download_url'] != null
@@ -77,15 +77,15 @@ class Codeberg extends AppSource {
: const MapEntry('', ''); : const MapEntry('', '');
}) })
.where((element) => element.key.toLowerCase().endsWith('.apk')) .where((element) => element.key.toLowerCase().endsWith('.apk'))
.map((e) => e.value)
.toList() ?? .toList() ??
[]; [];
dynamic targetRelease; dynamic targetRelease;
var prerrelsSkipped = 0;
for (int i = 0; i < releases.length; i++) { for (int i = 0; i < releases.length; i++) {
if (!fallbackToOlderReleases && i > 0) break; if (!fallbackToOlderReleases && i > prerrelsSkipped) break;
if (!includePrereleases && releases[i]['prerelease'] == true) { if (!includePrereleases && releases[i]['prerelease'] == true) {
prerrelsSkipped++;
continue; continue;
} }
if (releases[i]['draft'] == true) { if (releases[i]['draft'] == true) {
@@ -119,7 +119,9 @@ class Codeberg extends AppSource {
throw NoVersionError(); throw NoVersionError();
} }
var changeLog = targetRelease['body'].toString(); var changeLog = targetRelease['body'].toString();
return APKDetails(version, targetRelease['apkUrls'] as List<String>, return APKDetails(
version,
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
getAppNames(standardUrl), getAppNames(standardUrl),
releaseDate: releaseDate, releaseDate: releaseDate,
changeLog: changeLog.isEmpty ? null : changeLog); changeLog: changeLog.isEmpty ? null : changeLog);

View File

@@ -14,12 +14,14 @@ class FDroid extends AppSource {
@override @override
String standardizeURL(String url) { String standardizeURL(String url) {
RegExp standardUrlRegExB = RegExp standardUrlRegExB =
RegExp('^https?://$host/+[^/]+/+packages/+[^/]+'); RegExp('^https?://(cloudflare\\.)?$host/+[^/]+/+packages/+[^/]+');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
if (match != null) { if (match != null) {
url = 'https://$host/packages/${Uri.parse(url).pathSegments.last}'; url =
'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
} }
RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+'); RegExp standardUrlRegExA =
RegExp('^https?://(cloudflare\\.)?$host/+packages/+[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase()); match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(name); throw InvalidURLError(name);
@@ -48,7 +50,7 @@ class FDroid extends AppSource {
.where((element) => element['versionName'] == latestVersion) .where((element) => element['versionName'] == latestVersion)
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList(); .toList();
return APKDetails(latestVersion, apkUrls, return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
AppNames(name, Uri.parse(standardUrl).pathSegments.last)); AppNames(name, Uri.parse(standardUrl).pathSegments.last));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
@@ -61,9 +63,10 @@ class FDroid extends AppSource {
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
String host = Uri.parse(standardUrl).host;
return getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), await get(Uri.parse('https://$host/api/v1/packages/$appId')),
'https://f-droid.org/repo/$appId', 'https://$host/repo/$appId',
standardUrl); standardUrl);
} }
} }

View File

@@ -80,7 +80,8 @@ class FDroidRepo extends AppSource {
element.querySelector('apkname') != null) element.querySelector('apkname') != null)
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}') .map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
.toList(); .toList();
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName), return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
AppNames(authorName, appName),
releaseDate: releaseDate); releaseDate: releaseDate);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@@ -127,10 +127,11 @@ class GitHub extends AppSource {
[]; [];
dynamic targetRelease; dynamic targetRelease;
var prerrelsSkipped = 0;
for (int i = 0; i < releases.length; i++) { for (int i = 0; i < releases.length; i++) {
if (!fallbackToOlderReleases && i > 0) break; if (!fallbackToOlderReleases && i > prerrelsSkipped) break;
if (!includePrereleases && releases[i]['prerelease'] == true) { if (!includePrereleases && releases[i]['prerelease'] == true) {
prerrelsSkipped++;
continue; continue;
} }
var nameToFilter = releases[i]['name'] as String?; var nameToFilter = releases[i]['name'] as String?;
@@ -161,7 +162,9 @@ class GitHub extends AppSource {
throw NoVersionError(); throw NoVersionError();
} }
var changeLog = targetRelease['body'].toString(); var changeLog = targetRelease['body'].toString();
return APKDetails(version, targetRelease['apkUrls'] as List<String>, return APKDetails(
version,
getApkUrlsFromUrls(targetRelease['apkUrls'] as List<String>),
getAppNames(standardUrl), getAppNames(standardUrl),
releaseDate: releaseDate, releaseDate: releaseDate,
changeLog: changeLog.isEmpty ? null : changeLog); changeLog: changeLog.isEmpty ? null : changeLog);
@@ -185,9 +188,11 @@ class GitHub extends AppSource {
Map<String, String> urlsWithDescriptions = {}; Map<String, String> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) { for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) {
urlsWithDescriptions.addAll({ urlsWithDescriptions.addAll({
e['html_url'] as String: e['description'] != null e['html_url'] as String:
? e['description'] as String ((e['archived'] == true ? '[ARCHIVED] ' : '') +
: tr('noDescription') (e['description'] != null
? e['description'] as String
: tr('noDescription')))
}); });
} }
return urlsWithDescriptions; return urlsWithDescriptions;

View File

@@ -60,7 +60,8 @@ class GitLab extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl), return APKDetails(version, getApkUrlsFromUrls(apkUrls),
GitHub().getAppNames(standardUrl),
releaseDate: releaseDate); releaseDate: releaseDate);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@@ -34,15 +34,22 @@ class HTML extends AppSource {
var rel = links.last; var rel = links.last;
var apkName = rel.split('/').last; var apkName = rel.split('/').last;
var version = apkName.substring(0, apkName.length - 4); var version = apkName.substring(0, apkName.length - 4);
List<String> apkUrls = [rel] List<String> apkUrls = [rel].map((e) {
.map((e) => e.toLowerCase().startsWith('http://') || try {
e.toLowerCase().startsWith('https://') Uri.parse(e).origin;
? e return e;
: e.startsWith('/') } catch (err) {
? '${uri.origin}/$e' // is relative
: '${uri.origin}/${uri.path}/$e') }
.toList(); var currPathSegments = uri.path.split('/');
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app'))); if (e.startsWith('/') || currPathSegments.isEmpty) {
return '${uri.origin}/$e';
} else {
return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
}
}).toList();
return APKDetails(
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -1,5 +1,6 @@
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/github.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';
@@ -29,19 +30,37 @@ class Mullvad extends AppSource {
) async { ) async {
Response res = await get(Uri.parse('$standardUrl/en/download/android')); Response res = await get(Uri.parse('$standardUrl/en/download/android'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var version = parse(res.body) var versions = parse(res.body)
.querySelector('p.subtitle.is-6') .querySelectorAll('p')
?.querySelector('a') .map((e) => e.innerHtml)
?.attributes['href'] .where((p) => p.contains('Latest version: '))
?.split('/') .map((e) {
.last; var match = RegExp('[0-9]+(\\.[0-9]+)*').firstMatch(e);
if (version == null) { if (match == null) {
return '';
} else {
return e.substring(match.start, match.end);
}
})
.where((element) => element.isNotEmpty)
.toList();
if (versions.isEmpty) {
throw NoVersionError(); throw NoVersionError();
} }
String? changeLog;
try {
changeLog = (await GitHub().getLatestAPKDetails(
'https://github.com/mullvad/mullvadvpn-app',
{'fallbackToOlderReleases': true}))
.changeLog;
} catch (e) {
// Ignore
}
return APKDetails( return APKDetails(
version, versions[0],
['https://mullvad.net/download/app/apk/latest'], getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']),
AppNames(name, 'Mullvad-VPN')); AppNames(name, 'Mullvad-VPN'),
changeLog: changeLog);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -98,7 +98,7 @@ class NeutronCode extends AppSource {
? (customDateParse(dateStringOriginal)) ? (customDateParse(dateStringOriginal))
: null; : null;
var changeLogElements = http.querySelectorAll('.pd-fdesc p'); var changeLogElements = http.querySelectorAll('.pd-fdesc p');
return APKDetails(version, [apkUrl], return APKDetails(version, getApkUrlsFromUrls([apkUrl]),
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last), AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
releaseDate: dateString != null ? DateTime.parse(dateString) : null, releaseDate: dateString != null ? DateTime.parse(dateString) : null,
changeLog: changeLogElements.isNotEmpty changeLog: changeLogElements.isNotEmpty

View File

@@ -28,7 +28,8 @@ class Signal extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, apkUrls, AppNames(name, 'Signal')); return APKDetails(
version, getApkUrlsFromUrls(apkUrls), AppNames(name, 'Signal'));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -31,7 +31,8 @@ class SourceForge extends AppSource {
getVersion(String url) { getVersion(String url) {
try { try {
var tokens = url.split('/'); var tokens = url.split('/');
return tokens[tokens.length - 3]; var fi = tokens.indexOf('files');
return tokens[tokens[fi + 2] == 'download' ? fi - 1 : fi + 1];
} catch (e) { } catch (e) {
return null; return null;
} }
@@ -50,7 +51,7 @@ class SourceForge extends AppSource {
.toList(); .toList();
return APKDetails( return APKDetails(
version, version,
apkUrlList, getApkUrlsFromUrls(apkUrlList),
AppNames( AppNames(
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1))); name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
} else { } else {

View File

@@ -53,7 +53,8 @@ class SteamMobile extends AppSource {
var version = links[0].substring( var version = links[0].substring(
versionMatch.start + apkNamePrefix.length + 2, versionMatch.end - 4); versionMatch.start + apkNamePrefix.length + 2, versionMatch.end - 4);
var apkUrls = [links[0]]; var apkUrls = [links[0]];
return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!)); return APKDetails(version, getApkUrlsFromUrls(apkUrls),
AppNames(name, apks[apkNamePrefix]!));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -32,7 +32,8 @@ class TelegramApp extends AppSource {
throw NoVersionError(); throw NoVersionError();
} }
String? apkUrl = 'https://telegram.org/dl/android/apk'; String? apkUrl = 'https://telegram.org/dl/android/apk';
return APKDetails(version, [apkUrl], AppNames('Telegram', 'Telegram')); return APKDetails(version, getApkUrlsFromUrls([apkUrl]),
AppNames('Telegram', 'Telegram'));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -54,7 +54,8 @@ class VLC extends AppSource {
throw getObtainiumHttpError(res2); throw getObtainiumHttpError(res2);
} }
return APKDetails(version, apkUrls, AppNames('VideoLAN', 'VLC')); return APKDetails(
version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC'));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -64,9 +64,9 @@ class WhatsApp extends AppSource {
vLines[0].substring(versionMatch.start, versionMatch.end); vLines[0].substring(versionMatch.start, versionMatch.end);
return APKDetails( return APKDetails(
version, version,
[ getApkUrlsFromUrls([
'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime' 'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime'
], ]),
AppNames('Meta', 'WhatsApp')); AppNames('Meta', 'WhatsApp'));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@@ -48,6 +48,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
class GeneratedFormDropdown extends GeneratedFormItem { class GeneratedFormDropdown extends GeneratedFormItem {
late List<MapEntry<String, String>>? opts; late List<MapEntry<String, String>>? opts;
List<String>? disabledOptKeys;
GeneratedFormDropdown( GeneratedFormDropdown(
String key, String key,
@@ -55,6 +56,7 @@ class GeneratedFormDropdown extends GeneratedFormItem {
String label = 'Input', String label = 'Input',
List<Widget> belowWidgets = const [], List<Widget> belowWidgets = const [],
String defaultValue = '', String defaultValue = '',
this.disabledOptKeys,
List<String? Function(String? value)> additionalValidators = const [], List<String? Function(String? value)> additionalValidators = const [],
}) : super(key, }) : super(key,
label: label, label: label,
@@ -225,10 +227,15 @@ class _GeneratedFormState extends State<GeneratedForm> {
return DropdownButtonFormField( return DropdownButtonFormField(
decoration: InputDecoration(labelText: formItem.label), decoration: InputDecoration(labelText: formItem.label),
value: values[formItem.key], value: values[formItem.key],
items: formItem.opts! items: formItem.opts!.map((e2) {
.map((e2) => var enabled =
DropdownMenuItem(value: e2.key, child: Text(e2.value))) formItem.disabledOptKeys?.contains(e2.key) != true;
.toList(), return DropdownMenuItem(
value: e2.key,
enabled: enabled,
child: Opacity(
opacity: enabled ? 1 : 0.5, child: Text(e2.value)));
}).toList(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
values[formItem.key] = value ?? formItem.opts!.first.key; values[formItem.key] = value ?? formItem.opts!.first.key;
@@ -260,7 +267,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
formInputs[r][e] = Row( formInputs[r][e] = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(widget.items[r][e].label), Flexible(child: Text(widget.items[r][e].label)),
const SizedBox(
width: 8,
),
Switch( Switch(
value: values[widget.items[r][e].key], value: values[widget.items[r][e].key],
onChanged: (value) { onChanged: (value) {

View File

@@ -21,7 +21,7 @@ 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.11.14'; const String currentVersion = '0.11.32';
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
@@ -147,6 +147,14 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
try {
ByteData data =
await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
SecurityContext.defaultContext
.setTrustedCertificatesBytes(data.buffer.asUint8List());
} catch (e) {
// Already added, do nothing (see #375)
}
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) { if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) {
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
@@ -255,6 +263,14 @@ class _ObtainiumState extends State<Obtainium> {
darkColorScheme = ColorScheme.fromSeed( darkColorScheme = ColorScheme.fromSeed(
seedColor: defaultThemeColour, brightness: Brightness.dark); seedColor: defaultThemeColour, brightness: Brightness.dark);
} }
// set the background and surface colors to pure black in the amoled theme
if (settingsProvider.useBlackTheme) {
darkColorScheme = darkColorScheme
.copyWith(background: Colors.black, surface: Colors.black)
.harmonized();
}
return MaterialApp( return MaterialApp(
title: 'Obtainium', title: 'Obtainium',
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,

View File

@@ -33,10 +33,10 @@ class _AddAppPageState extends State<AddAppPage> {
bool additionalSettingsValid = true; bool additionalSettingsValid = true;
List<String> pickedCategories = []; List<String> pickedCategories = [];
int searchnum = 0; int searchnum = 0;
SourceProvider sourceProvider = SourceProvider();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider();
AppsProvider appsProvider = context.read<AppsProvider>(); AppsProvider appsProvider = context.read<AppsProvider>();
bool doingSomething = gettingAppInfo || searching; bool doingSomething = gettingAppInfo || searching;
@@ -64,65 +64,56 @@ class _AddAppPageState extends State<AddAppPage> {
} }
} }
getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async {
return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('xIsTrackOnly', args: [
pickedSource!.enforceTrackOnly
? tr('source')
: tr('app')
]),
items: const [],
message:
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
);
}) ==
null));
}
getReleaseDateAsVersionConfirmationIfNeeded(
bool userPickedTrackOnly) async {
return (!(additionalSettings['versionDetection'] ==
'releaseDateAsVersion' &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('releaseDateAsVersion'),
items: const [],
message: tr('releaseDateAsVersionExplanation'),
);
}) ==
null));
}
addApp({bool resetUserInputAfter = false}) async { addApp({bool resetUserInputAfter = false}) async {
setState(() { setState(() {
gettingAppInfo = true; gettingAppInfo = true;
}); });
var settingsProvider = context.read<SettingsProvider>(); try {
() async { var settingsProvider = context.read<SettingsProvider>();
var userPickedTrackOnly = additionalSettings['trackOnly'] == true; var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
var cont = true; App? app;
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
// ignore: use_build_context_synchronously (await getReleaseDateAsVersionConfirmationIfNeeded(
await showDialog( userPickedTrackOnly))) {
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('xIsTrackOnly', args: [
pickedSource!.enforceTrackOnly
? tr('source')
: tr('app')
]),
items: const [],
message:
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
);
}) ==
null) {
cont = false;
}
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('releaseDateAsVersion'),
items: const [],
message: tr('releaseDateAsVersionExplanation'),
);
}) ==
null) {
cont = false;
}
if (additionalSettings['versionDetection'] == 'noVersionDetection' &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('disableVersionDetection'),
items: const [],
message: tr('noVersionDetectionExplanation'),
);
}) ==
null) {
cont = false;
}
if (cont) {
HapticFeedback.selectionClick();
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
App app = await sourceProvider.getApp( app = await sourceProvider.getApp(
pickedSource!, userInput, additionalSettings, pickedSource!, userInput, additionalSettings,
trackOnlyOverride: trackOnly); trackOnlyOverride: trackOnly);
if (!trackOnly) { if (!trackOnly) {
@@ -150,261 +141,252 @@ class _AddAppPageState extends State<AddAppPage> {
} }
app.categories = pickedCategories; app.categories = pickedCategories;
await appsProvider.saveApps([app], onlyIfExists: false); await appsProvider.saveApps([app], onlyIfExists: false);
return app;
} }
}()
.then((app) {
if (app != null) { if (app != null) {
Navigator.push(globalNavigatorKey.currentContext ?? context, Navigator.push(globalNavigatorKey.currentContext ?? context,
MaterialPageRoute(builder: (context) => AppPage(appId: app.id))); MaterialPageRoute(builder: (context) => AppPage(appId: app!.id)));
} }
}).catchError((e) { } catch (e) {
showError(e, context); showError(e, context);
}).whenComplete(() { } finally {
setState(() { setState(() {
gettingAppInfo = false; gettingAppInfo = false;
if (resetUserInputAfter) { if (resetUserInputAfter) {
changeUserInput('', false, true); changeUserInput('', false, true);
} }
}); });
}); }
} }
Widget getUrlInputRow() => Row(
children: [
Expanded(
child: GeneratedForm(
key: Key(searchnum.toString()),
items: [
[
GeneratedFormTextField('appSourceURL',
label: tr('appSourceURL'),
defaultValue: userInput,
additionalValidators: [
(value) {
try {
sourceProvider
.getSource(value ?? '')
.standardizeURL(
preStandardizeUrl(value ?? ''));
} catch (e) {
return e is String
? e
: e is ObtainiumError
? e.toString()
: tr('error');
}
return null;
}
])
]
],
onValueChanges: (values, valid, isBuilding) {
changeUserInput(
values['appSourceURL']!, valid, isBuilding);
})),
const SizedBox(
width: 16,
),
gettingAppInfo
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: doingSomething ||
pickedSource == null ||
(pickedSource!.combinedAppSpecificSettingFormItems
.isNotEmpty &&
!additionalSettingsValid)
? null
: () {
HapticFeedback.selectionClick();
addApp();
},
child: Text(tr('add')))
],
);
runSearch() async {
setState(() {
searching = true;
});
try {
var results = await Future.wait(sourceProvider.sources
.where((e) => e.canSearch)
.map((e) => e.search(searchQuery)));
// .then((results) async {
// Interleave results instead of simple reduce
Map<String, String> res = {};
var si = 0;
var done = false;
while (!done) {
done = true;
for (var r in results) {
if (r.length > si) {
done = false;
res.addEntries([r.entries.elementAt(si)]);
}
}
si++;
}
List<String>? selectedUrls = res.isEmpty
? []
// ignore: use_build_context_synchronously
: await showDialog<List<String>?>(
context: context,
builder: (BuildContext ctx) {
return UrlSelectionModal(
urlsWithDescriptions: res,
selectedByDefault: false,
onlyOneSelectionAllowed: true,
);
});
if (selectedUrls != null && selectedUrls.isNotEmpty) {
changeUserInput(selectedUrls[0], true, false, isSearch: true);
}
} catch (e) {
showError(e, context);
} finally {
setState(() {
searching = false;
});
}
}
bool shouldShowSearchBar() =>
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
pickedSource == null &&
userInput.isEmpty;
Widget getSearchBarRow() => Row(
children: [
Expanded(
child: GeneratedForm(
items: [
[
GeneratedFormTextField('searchSomeSources',
label: tr('searchSomeSourcesLabel'), required: false),
]
],
onValueChanges: (values, valid, isBuilding) {
if (values.isNotEmpty && valid && !isBuilding) {
setState(() {
searchQuery = values['searchSomeSources']!.trim();
});
}
}),
),
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: searchQuery.isEmpty || doingSomething
? null
: () {
runSearch();
},
child: Text(tr('search')))
],
);
Widget getAdditionalOptsCol() => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Divider(
height: 64,
),
Text(
tr('additionalOptsFor',
args: [pickedSource?.name ?? tr('source')]),
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
const SizedBox(
height: 16,
),
GeneratedForm(
key: Key(pickedSource.runtimeType.toString()),
items: pickedSource!.combinedAppSpecificSettingFormItems,
onValueChanges: (values, valid, isBuilding) {
if (!isBuilding) {
setState(() {
additionalSettings = values;
additionalSettingsValid = valid;
});
}
}),
Column(
children: [
const SizedBox(
height: 16,
),
CategoryEditorSelector(
alignment: WrapAlignment.start,
onSelected: (categories) {
pickedCategories = categories;
}),
],
),
],
);
Widget getSourcesListWidget() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 48,
),
Text(
tr('supportedSourcesBelow'),
),
const SizedBox(
height: 8,
),
...sourceProvider.sources
.map((e) => GestureDetector(
onTap: e.host != null
? () {
launchUrlString('https://${e.host}',
mode: LaunchMode.externalApplication);
}
: null,
child: Text(
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
style: TextStyle(
decoration: e.host != null
? TextDecoration.underline
: TextDecoration.none,
fontStyle: FontStyle.italic),
)))
.toList()
]);
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: CustomScrollView(slivers: <Widget>[ body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
CustomAppBar(title: tr('addApp')), CustomAppBar(title: tr('addApp')),
SliverFillRemaining( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( getUrlInputRow(),
children: [ if (shouldShowSearchBar())
Expanded(
child: GeneratedForm(
key: Key(searchnum.toString()),
items: [
[
GeneratedFormTextField('appSourceURL',
label: tr('appSourceURL'),
defaultValue: userInput,
additionalValidators: [
(value) {
try {
sourceProvider
.getSource(value ?? '')
.standardizeURL(
preStandardizeUrl(
value ?? ''));
} catch (e) {
return e is String
? e
: e is ObtainiumError
? e.toString()
: tr('error');
}
return null;
}
])
]
],
onValueChanges: (values, valid, isBuilding) {
changeUserInput(values['appSourceURL']!,
valid, isBuilding);
})),
const SizedBox(
width: 16,
),
gettingAppInfo
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: doingSomething ||
pickedSource == null ||
(pickedSource!
.combinedAppSpecificSettingFormItems
.isNotEmpty &&
!additionalSettingsValid)
? null
: addApp,
child: Text(tr('add')))
],
),
if (sourceProvider.sources
.where((e) => e.canSearch)
.isNotEmpty &&
pickedSource == null &&
userInput.isEmpty)
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
if (sourceProvider.sources if (shouldShowSearchBar()) getSearchBarRow(),
.where((e) => e.canSearch)
.isNotEmpty &&
pickedSource == null &&
userInput.isEmpty)
Row(
children: [
Expanded(
child: GeneratedForm(
items: [
[
GeneratedFormTextField(
'searchSomeSources',
label: tr('searchSomeSourcesLabel'),
required: false),
]
],
onValueChanges: (values, valid, isBuilding) {
if (values.isNotEmpty &&
valid &&
!isBuilding) {
setState(() {
searchQuery =
values['searchSomeSources']!.trim();
});
}
}),
),
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: searchQuery.isEmpty || doingSomething
? null
: () {
setState(() {
searching = true;
});
Future.wait(sourceProvider.sources
.where((e) => e.canSearch)
.map((e) =>
e.search(searchQuery)))
.then((results) async {
// Interleave results instead of simple reduce
Map<String, String> res = {};
var si = 0;
var done = false;
while (!done) {
done = true;
for (var r in results) {
if (r.length > si) {
done = false;
res.addEntries(
[r.entries.elementAt(si)]);
}
}
si++;
}
List<String>? selectedUrls = res
.isEmpty
? []
: await showDialog<List<String>?>(
context: context,
builder: (BuildContext ctx) {
return UrlSelectionModal(
urlsWithDescriptions: res,
selectedByDefault: false,
onlyOneSelectionAllowed:
true,
);
});
if (selectedUrls != null &&
selectedUrls.isNotEmpty) {
changeUserInput(
selectedUrls[0], true, false,
isSearch: true);
}
}).catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
searching = false;
});
});
},
child: Text(tr('search')))
],
),
if (pickedSource != null) if (pickedSource != null)
Column( getAdditionalOptsCol()
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Divider(
height: 64,
),
Text(
tr('additionalOptsFor',
args: [pickedSource?.name ?? tr('source')]),
style: TextStyle(
color:
Theme.of(context).colorScheme.primary)),
const SizedBox(
height: 16,
),
GeneratedForm(
key: Key(pickedSource.runtimeType.toString()),
items: pickedSource!
.combinedAppSpecificSettingFormItems,
onValueChanges: (values, valid, isBuilding) {
if (!isBuilding) {
setState(() {
additionalSettings = values;
additionalSettingsValid = valid;
});
}
}),
Column(
children: [
const SizedBox(
height: 16,
),
CategoryEditorSelector(
alignment: WrapAlignment.start,
onSelected: (categories) {
pickedCategories = categories;
}),
],
),
],
)
else else
Expanded( getSourcesListWidget(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 48,
),
Text(
tr('supportedSourcesBelow'),
),
const SizedBox(
height: 8,
),
...sourceProvider.sources
.map((e) => GestureDetector(
onTap: e.host != null
? () {
launchUrlString(
'https://${e.host}',
mode: LaunchMode
.externalApplication);
}
: null,
child: Text(
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
style: TextStyle(
decoration: e.host != null
? TextDecoration.underline
: TextDecoration.none,
fontStyle: FontStyle.italic),
)))
.toList()
])),
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),

View File

@@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart'; import 'package:obtainium/main.dart';
@@ -34,406 +35,420 @@ class _AppPageState extends State<AppPage> {
}); });
} }
bool areDownloadsRunning = appsProvider.areDownloadsRunning();
var sourceProvider = SourceProvider(); var sourceProvider = SourceProvider();
AppInMemory? app = appsProvider.apps[widget.appId]; AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
var source = app != null ? sourceProvider.getSource(app.app.url) : null; var source = app != null ? sourceProvider.getSource(app.app.url) : null;
if (!appsProvider.areDownloadsRunning() && prevApp == null && app != null) { if (!areDownloadsRunning && prevApp == null && app != null) {
prevApp = app; prevApp = app;
getUpdate(app.app.id); getUpdate(app.app.id);
} }
var trackOnly = app?.app.additionalSettings['trackOnly'] == true; var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
var infoColumn = Column( bool isVersionDetectionStandard =
mainAxisAlignment: MainAxisAlignment.center, app?.app.additionalSettings['versionDetection'] ==
crossAxisAlignment: CrossAxisAlignment.stretch, 'standardVersionDetection';
children: [
GestureDetector(
onTap: () {
if (app?.app.url != null) {
launchUrlString(app?.app.url ?? '',
mode: LaunchMode.externalApplication);
}
},
child: Text(
app?.app.url ?? '',
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic,
fontSize: 12),
)),
const SizedBox(
height: 32,
),
Text(
tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'${tr('installedVersionX', args: [
app?.app.installedVersion ?? tr('none')
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
tr('app')
])}' : ''}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(
height: 32,
),
Text(
tr('lastUpdateCheckX', args: [
app?.app.lastUpdateCheck == null
? tr('never')
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
]),
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
),
const SizedBox(
height: 48,
),
CategoryEditorSelector(
alignment: WrapAlignment.center,
preselected:
app?.app.categories != null ? app!.app.categories.toSet() : {},
onSelected: (categories) {
if (app != null) {
app.app.categories = categories;
appsProvider.saveApps([app.app]);
}
}),
],
);
var fullInfoColumn = Column( getInfoColumn() => Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 125), GestureDetector(
app?.installedInfo != null onTap: () {
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ if (app?.app.url != null) {
Image.memory( launchUrlString(app?.app.url ?? '',
app!.installedInfo!.icon!, mode: LaunchMode.externalApplication);
height: 150, }
gaplessPlayback: true, },
) onLongPress: () {
]) Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
: Container(), ScaffoldMessenger.of(context).showSnackBar(SnackBar(
const SizedBox( content: Text(tr('copiedToClipboard')),
height: 25, ));
), },
Text( child: Text(
app?.installedInfo?.name ?? app?.app.name ?? tr('app'), app?.app.url ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayLarge, style: const TextStyle(
), decoration: TextDecoration.underline,
Text( fontStyle: FontStyle.italic,
tr('byX', args: [app?.app.author ?? tr('unknown')]), fontSize: 12),
textAlign: TextAlign.center, )),
style: Theme.of(context).textTheme.headlineMedium, const SizedBox(
), height: 32,
const SizedBox( ),
height: 8, Text(
), tr('latestVersionX',
Text( args: [app?.app.latestVersion ?? tr('unknown')]),
app?.app.id ?? '', textAlign: TextAlign.center,
textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge,
style: Theme.of(context).textTheme.labelSmall, ),
), Text(
app?.app.releaseDate == null '${tr('installedVersionX', args: [
? const SizedBox.shrink() app?.app.installedVersion ?? tr('none')
: Text( ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
app!.app.releaseDate.toString(), tr('app')
textAlign: TextAlign.center, ])}' : ''}',
style: Theme.of(context).textTheme.labelSmall, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
if (app?.app.installedVersion != null &&
!isVersionDetectionStandard)
Column(
children: [
const SizedBox(
height: 4,
),
Text(
tr('noVersionDetection'),
style: Theme.of(context).textTheme.labelSmall,
)
],
), ),
const SizedBox( const SizedBox(
height: 32, height: 32,
), ),
infoColumn, Text(
const SizedBox(height: 150) tr('lastUpdateCheckX', args: [
], app?.app.lastUpdateCheck == null
); ? tr('never')
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
]),
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
),
const SizedBox(
height: 48,
),
CategoryEditorSelector(
alignment: WrapAlignment.center,
preselected: app?.app.categories != null
? app!.app.categories.toSet()
: {},
onSelected: (categories) {
if (app != null) {
app.app.categories = categories;
appsProvider.saveApps([app.app]);
}
}),
],
);
getFullInfoColumn() => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 125),
app?.installedInfo != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory(
app!.installedInfo!.icon!,
height: 150,
gaplessPlayback: true,
)
])
: Container(),
const SizedBox(
height: 25,
),
Text(
app?.name ?? tr('app'),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayLarge,
),
Text(
tr('byX', args: [app?.app.author ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(
height: 8,
),
Text(
app?.app.id ?? '',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
app?.app.releaseDate == null
? const SizedBox.shrink()
: Text(
app!.app.releaseDate.toString(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(
height: 32,
),
getInfoColumn(),
const SizedBox(height: 150)
],
);
getAppWebView() => app != null
? WebViewWidget(
controller: WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Theme.of(context).colorScheme.background)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onWebResourceError: (WebResourceError error) {
if (error.isForMainFrame == true) {
showError(
ObtainiumError(error.description, unexpected: true),
context);
}
},
),
)
..loadRequest(Uri.parse(app.app.url)))
: Container();
showMarkUpdatedDialog() {
return showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: Text(tr('alreadyUpToDateQuestion')),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(tr('no'))),
TextButton(
onPressed: () {
HapticFeedback.selectionClick();
var updatedApp = app?.app;
if (updatedApp != null) {
updatedApp.installedVersion = updatedApp.latestVersion;
appsProvider.saveApps([updatedApp]);
}
Navigator.of(context).pop();
},
child: Text(tr('yesMarkUpdated')))
],
);
});
}
showAdditionalOptionsDialog() async {
return await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var items =
(source?.combinedAppSpecificSettingFormItems ?? []).map((row) {
row = row.map((e) {
if (app?.app.additionalSettings[e.key] != null) {
e.defaultValue = app?.app.additionalSettings[e.key];
}
return e;
}).toList();
return row;
}).toList();
items = items.map((row) {
row = row.map((e) {
if (e.key == 'versionDetection' && e is GeneratedFormDropdown) {
e.disabledOptKeys ??= [];
if (app?.app.installedVersion != null &&
app?.app.additionalSettings['versionDetection'] !=
'releaseDateAsVersion' &&
!appsProvider.isVersionDetectionPossible(app)) {
e.disabledOptKeys!.add('standardVersionDetection');
}
if (app?.app.releaseDate == null) {
e.disabledOptKeys!.add('releaseDateAsVersion');
}
}
return e;
}).toList();
return row;
}).toList();
return GeneratedFormModal(
title: tr('additionalOptions'),
items: items,
);
});
}
handleAdditionalOptionChanges(Map<String, dynamic>? values) {
if (app != null && values != null) {
Map<String, dynamic> originalSettings = app.app.additionalSettings;
app.app.additionalSettings = values;
if (source?.enforceTrackOnly == true) {
app.app.additionalSettings['trackOnly'] = true;
// ignore: use_build_context_synchronously
showError(tr('appsFromSourceAreTrackOnly'), context);
}
if (app.app.additionalSettings['versionDetection'] ==
'releaseDateAsVersion') {
if (originalSettings['versionDetection'] != 'releaseDateAsVersion') {
if (app.app.releaseDate != null) {
bool isUpdated =
app.app.installedVersion == app.app.latestVersion;
app.app.latestVersion =
app.app.releaseDate!.microsecondsSinceEpoch.toString();
if (isUpdated) {
app.app.installedVersion = app.app.latestVersion;
}
}
}
} else if (originalSettings['versionDetection'] ==
'releaseDateAsVersion') {
app.app.installedVersion =
app.installedInfo?.versionName ?? app.app.installedVersion;
}
appsProvider.saveApps([app.app]).then((value) {
getUpdate(app.app.id);
});
}
}
getInstallOrUpdateButton() => TextButton(
onPressed: (app?.app.installedVersion == null ||
app?.app.installedVersion != app?.app.latestVersion) &&
!areDownloadsRunning
? () async {
try {
HapticFeedback.heavyImpact();
if (app?.app.additionalSettings['trackOnly'] != true) {
await settingsProvider.getInstallPermission();
}
var res = await appsProvider.downloadAndInstallLatestApps(
[app!.app.id], globalNavigatorKey.currentContext);
if (res.isNotEmpty && mounted) {
Navigator.of(context).pop();
}
} catch (e) {
showError(e, context);
}
}
: null,
child: Text(app?.app.installedVersion == null
? !trackOnly
? tr('install')
: tr('markInstalled')
: !trackOnly
? tr('update')
: tr('markUpdated')));
getBottomSheetMenu() => Padding(
padding:
EdgeInsets.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (app?.app.installedVersion != null &&
app?.app.installedVersion != app?.app.latestVersion &&
!isVersionDetectionStandard &&
!trackOnly)
IconButton(
onPressed: app?.downloadProgress != null
? null
: showMarkUpdatedDialog,
tooltip: tr('markUpdated'),
icon: const Icon(Icons.done)),
if (source != null &&
source.combinedAppSpecificSettingFormItems.isNotEmpty)
IconButton(
onPressed: app?.downloadProgress != null
? null
: () async {
var values =
await showAdditionalOptionsDialog();
handleAdditionalOptionChanges(values);
},
tooltip: tr('additionalOptions'),
icon: const Icon(Icons.edit)),
if (app != null && app.installedInfo != null)
IconButton(
onPressed: () {
appsProvider.openAppSettings(app.app.id);
},
icon: const Icon(Icons.settings),
tooltip: tr('settings'),
),
if (app != null && settingsProvider.showAppWebpage)
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
scrollable: true,
content: getInfoColumn(),
title: Text(
'${app.name} ${tr('byX', args: [
app.app.author
])}'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(tr('continue')))
],
);
});
},
icon: const Icon(Icons.more_horiz),
tooltip: tr('more')),
const SizedBox(width: 16.0),
Expanded(child: getInstallOrUpdateButton()),
const SizedBox(width: 16.0),
Expanded(
child: TextButton(
onPressed: app?.downloadProgress != null
? null
: () {
appsProvider.removeAppsWithModal(
context, [app!.app]).then((value) {
if (value == true) {
Navigator.of(context).pop();
}
});
},
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.error,
surfaceTintColor:
Theme.of(context).colorScheme.error),
child: Text(tr('remove')),
)),
])),
if (app?.downloadProgress != null)
Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
child: LinearProgressIndicator(
value: app!.downloadProgress! / 100))
],
));
return Scaffold( return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null, appBar: settingsProvider.showAppWebpage ? AppBar() : null,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: RefreshIndicator( body: RefreshIndicator(
child: settingsProvider.showAppWebpage child: settingsProvider.showAppWebpage
? app != null ? getAppWebView()
? WebViewWidget( : CustomScrollView(
controller: WebViewController() slivers: [
..setJavaScriptMode(JavaScriptMode.unrestricted) SliverToBoxAdapter(
..setBackgroundColor( child: Column(children: [getFullInfoColumn()])),
Theme.of(context).colorScheme.background) ],
..setJavaScriptMode(JavaScriptMode.unrestricted) ),
..setNavigationDelegate( onRefresh: () async {
NavigationDelegate( if (app != null) {
onWebResourceError: (WebResourceError error) { getUpdate(app.app.id);
if (error.isForMainFrame == true) { }
showError( }),
ObtainiumError(error.description, bottomSheet: getBottomSheetMenu());
unexpected: true),
context);
}
},
),
)
..loadRequest(Uri.parse(app.app.url)))
: Container()
: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(children: [fullInfoColumn])),
],
),
onRefresh: () async {
if (app != null) {
getUpdate(app.app.id);
}
}),
bottomSheet: Padding(
padding: EdgeInsets.fromLTRB(
0, 0, 0, MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (app?.app.additionalSettings['versionDetection'] !=
'standardVersionDetection' &&
!trackOnly &&
app?.app.installedVersion != null &&
app?.app.installedVersion != app?.app.latestVersion)
IconButton(
onPressed: app?.downloadProgress != null
? null
: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: Text(tr(
'alreadyUpToDateQuestion')),
actions: [
TextButton(
onPressed: () {
Navigator.of(context)
.pop();
},
child: Text(tr('no'))),
TextButton(
onPressed: () {
HapticFeedback
.selectionClick();
var updatedApp = app?.app;
if (updatedApp != null) {
updatedApp
.installedVersion =
updatedApp
.latestVersion;
appsProvider.saveApps(
[updatedApp]);
}
Navigator.of(context)
.pop();
},
child: Text(
tr('yesMarkUpdated')))
],
);
});
},
tooltip: tr('markUpdated'),
icon: const Icon(Icons.done)),
if (source != null &&
source
.combinedAppSpecificSettingFormItems.isNotEmpty)
IconButton(
onPressed: app?.downloadProgress != null
? null
: () {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var items = source
.combinedAppSpecificSettingFormItems
.map((row) {
row.map((e) {
if (app?.app.additionalSettings[
e.key] !=
null) {
e.defaultValue = app?.app
.additionalSettings[
e.key];
}
return e;
}).toList();
return row;
}).toList();
return GeneratedFormModal(
title: tr('additionalOptions'),
items: items,
);
}).then((values) {
if (app != null && values != null) {
Map<String, dynamic>
originalSettings =
app.app.additionalSettings;
app.app.additionalSettings = values;
if (source.enforceTrackOnly) {
app.app.additionalSettings[
'trackOnly'] = true;
showError(
tr('appsFromSourceAreTrackOnly'),
context);
}
if (app.app.additionalSettings[
'versionDetection'] ==
'releaseDateAsVersion') {
if (originalSettings[
'versionDetection'] !=
'releaseDateAsVersion') {
if (app.app.releaseDate != null) {
bool isUpdated =
app.app.installedVersion ==
app.app.latestVersion;
app.app.latestVersion = app
.app
.releaseDate!
.microsecondsSinceEpoch
.toString();
if (isUpdated) {
app.app.installedVersion =
app.app.latestVersion;
}
}
}
} else if (originalSettings[
'versionDetection'] ==
'releaseDateAsVersion') {
app.app.installedVersion = app
.installedInfo
?.versionName ??
app.app.installedVersion;
}
appsProvider.saveApps([app.app]).then(
(value) {
getUpdate(app.app.id);
});
}
});
},
tooltip: tr('additionalOptions'),
icon: const Icon(Icons.edit)),
if (app != null && app.installedInfo != null)
IconButton(
onPressed: () {
appsProvider.openAppSettings(app.app.id);
},
icon: const Icon(Icons.settings),
tooltip: tr('settings'),
),
if (app != null && settingsProvider.showAppWebpage)
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
scrollable: true,
content: infoColumn,
title: Text(
'${app.app.name} ${tr('byX', args: [
app.app.author
])}'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(tr('continue')))
],
);
});
},
icon: const Icon(Icons.more_horiz),
tooltip: tr('more')),
const SizedBox(width: 16.0),
Expanded(
child: TextButton(
onPressed: (app?.app.installedVersion == null ||
app?.app.installedVersion !=
app?.app.latestVersion) &&
!appsProvider.areDownloadsRunning()
? () {
HapticFeedback.heavyImpact();
() async {
if (app?.app.additionalSettings[
'trackOnly'] !=
true) {
await settingsProvider
.getInstallPermission();
}
}()
.then((value) {
appsProvider
.downloadAndInstallLatestApps(
[app!.app.id],
globalNavigatorKey
.currentContext).then(
(res) {
if (res.isNotEmpty && mounted) {
Navigator.of(context).pop();
}
}).catchError((e) {
showError(e, context);
});
}).catchError((e) {
showError(e, context);
});
}
: null,
child: Text(app?.app.installedVersion == null
? !trackOnly
? tr('install')
: tr('markInstalled')
: !trackOnly
? tr('update')
: tr('markUpdated')))),
const SizedBox(width: 16.0),
Expanded(
child: TextButton(
onPressed: app?.downloadProgress != null
? null
: () {
appsProvider.removeAppsWithModal(
context, [app!.app]).then((value) {
if (value == true) {
Navigator.of(context).pop();
}
});
},
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.error,
surfaceTintColor:
Theme.of(context).colorScheme.error),
child: Text(tr('remove')),
)),
])),
if (app?.downloadProgress != null)
Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
child: LinearProgressIndicator(
value: app!.downloadProgress! / 100))
],
)),
);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>(); var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>(); var settingsProvider = context.read<SettingsProvider>();
var outlineButtonStyle = ButtonStyle( var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
StadiumBorder( StadiumBorder(
@@ -101,6 +102,193 @@ class _ImportExportPageState extends State<ImportExportPage> {
}); });
} }
runObtainiumExport() {
HapticFeedback.selectionClick();
appsProvider.exportApps().then((String path) {
showError(tr('exportedTo', args: [path]), context);
}).catchError((e) {
showError(e, context);
});
}
runObtainiumImport() {
HapticFeedback.selectionClick();
FilePicker.platform.pickFiles().then((result) {
setState(() {
importInProgress = true;
});
if (result != null) {
String data = File(result.files.single.path!).readAsStringSync();
try {
jsonDecode(data);
} catch (e) {
throw ObtainiumError(tr('invalidInput'));
}
appsProvider.importApps(data).then((value) {
var cats = settingsProvider.categories;
appsProvider.apps.forEach((key, value) {
for (var c in value.app.categories) {
if (!cats.containsKey(c)) {
cats[c] = generateRandomLightColor().value;
}
}
});
appsProvider.addMissingCategories(settingsProvider);
showError(tr('importedX', args: [plural('apps', value)]), context);
});
} else {
// User canceled the picker
}
}).catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
importInProgress = false;
});
});
}
runUrlImport() {
FilePicker.platform.pickFiles().then((result) {
if (result != null) {
urlListImport(
overrideInitValid: true,
initValue: RegExp('https?://[^"]+')
.allMatches(
File(result.files.single.path!).readAsStringSync())
.map((e) => e.input.substring(e.start, e.end))
.toSet()
.toList()
.where((url) {
try {
sourceProvider.getSource(url);
return true;
} catch (e) {
return false;
}
}).join('\n'));
}
});
}
runSourceSearch(AppSource source) {
() async {
var values = await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('searchX', args: [source.name]),
items: [
[
GeneratedFormTextField('searchQuery',
label: tr('searchQuery'))
]
],
);
});
if (values != null &&
(values['searchQuery'] as String?)?.isNotEmpty == true) {
setState(() {
importInProgress = true;
});
var urlsWithDescriptions =
await source.search(values['searchQuery'] as String);
if (urlsWithDescriptions.isNotEmpty) {
var selectedUrls =
// ignore: use_build_context_synchronously
await showDialog<List<String>?>(
context: context,
builder: (BuildContext ctx) {
return UrlSelectionModal(
urlsWithDescriptions: urlsWithDescriptions,
selectedByDefault: false,
);
});
if (selectedUrls != null && selectedUrls.isNotEmpty) {
var errors = await appsProvider.addAppsByURL(selectedUrls);
if (errors.isEmpty) {
// ignore: use_build_context_synchronously
showError(
tr('importedX', args: [plural('app', selectedUrls.length)]),
context);
} else {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (BuildContext ctx) {
return ImportErrorDialog(
urlsLength: selectedUrls.length, errors: errors);
});
}
}
} else {
throw ObtainiumError(tr('noResults'));
}
}
}()
.catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
importInProgress = false;
});
});
}
runMassSourceImport(MassAppUrlSource source) {
() async {
var values = await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('importX', args: [source.name]),
items: source.requiredArgs
.map((e) => [GeneratedFormTextField(e, label: e)])
.toList(),
);
});
if (values != null) {
setState(() {
importInProgress = true;
});
var urlsWithDescriptions = await source.getUrlsWithDescriptions(
values.values.map((e) => e.toString()).toList());
var selectedUrls =
// ignore: use_build_context_synchronously
await showDialog<List<String>?>(
context: context,
builder: (BuildContext ctx) {
return UrlSelectionModal(
urlsWithDescriptions: urlsWithDescriptions);
});
if (selectedUrls != null) {
var errors = await appsProvider.addAppsByURL(selectedUrls);
if (errors.isEmpty) {
// ignore: use_build_context_synchronously
showError(
tr('importedX', args: [plural('app', selectedUrls.length)]),
context);
} else {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (BuildContext ctx) {
return ImportErrorDialog(
urlsLength: selectedUrls.length, errors: errors);
});
}
}
}
}()
.catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
importInProgress = false;
});
});
}
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: CustomScrollView(slivers: <Widget>[ body: CustomScrollView(slivers: <Widget>[
@@ -120,18 +308,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
onPressed: appsProvider.apps.isEmpty || onPressed: appsProvider.apps.isEmpty ||
importInProgress importInProgress
? null ? null
: () { : runObtainiumExport,
HapticFeedback.selectionClick();
appsProvider
.exportApps()
.then((String path) {
showError(
tr('exportedTo', args: [path]),
context);
}).catchError((e) {
showError(e, context);
});
},
child: Text(tr('obtainiumExport')))), child: Text(tr('obtainiumExport')))),
const SizedBox( const SizedBox(
width: 16, width: 16,
@@ -141,59 +318,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
style: outlineButtonStyle, style: outlineButtonStyle,
onPressed: importInProgress onPressed: importInProgress
? null ? null
: () { : runObtainiumImport,
HapticFeedback.selectionClick();
FilePicker.platform
.pickFiles()
.then((result) {
setState(() {
importInProgress = true;
});
if (result != null) {
String data = File(
result.files.single.path!)
.readAsStringSync();
try {
jsonDecode(data);
} catch (e) {
throw ObtainiumError(
tr('invalidInput'));
}
appsProvider
.importApps(data)
.then((value) {
var cats =
settingsProvider.categories;
appsProvider.apps
.forEach((key, value) {
for (var c
in value.app.categories) {
if (!cats.containsKey(c)) {
cats[c] =
generateRandomLightColor()
.value;
}
}
});
settingsProvider.categories =
cats;
showError(
tr('importedX', args: [
plural('apps', value)
]),
context);
});
} else {
// User canceled the picker
}
}).catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
importInProgress = false;
});
});
},
child: Text(tr('obtainiumImport')))) child: Text(tr('obtainiumImport'))))
], ],
), ),
@@ -216,49 +341,15 @@ class _ImportExportPageState extends State<ImportExportPage> {
height: 32, height: 32,
), ),
TextButton( TextButton(
onPressed: importInProgress onPressed:
? null importInProgress ? null : urlListImport,
: () {
urlListImport();
},
child: Text( child: Text(
tr('importFromURLList'), tr('importFromURLList'),
)), )),
const SizedBox(height: 8), const SizedBox(height: 8),
TextButton( TextButton(
onPressed: importInProgress onPressed:
? null importInProgress ? null : runUrlImport,
: () {
FilePicker.platform
.pickFiles()
.then((result) {
if (result != null) {
urlListImport(
overrideInitValid: true,
initValue:
RegExp('https?://[^"]+')
.allMatches(File(result
.files
.single
.path!)
.readAsStringSync())
.map((e) =>
e.input.substring(
e.start, e.end))
.toSet()
.toList()
.where((url) {
try {
sourceProvider
.getSource(url);
return true;
} catch (e) {
return false;
}
}).join('\n'));
}
});
},
child: Text( child: Text(
tr('importFromURLsInFile'), tr('importFromURLsInFile'),
)), )),
@@ -275,106 +366,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
onPressed: importInProgress onPressed: importInProgress
? null ? null
: () { : () {
() async { runSourceSearch(source);
var values = await showDialog<
Map<String,
dynamic>?>(
context: context,
builder:
(BuildContext ctx) {
return GeneratedFormModal(
title: tr('searchX',
args: [
source.name
]),
items: [
[
GeneratedFormTextField(
'searchQuery',
label: tr(
'searchQuery'))
]
],
);
});
if (values != null &&
(values['searchQuery']
as String?)
?.isNotEmpty ==
true) {
setState(() {
importInProgress = true;
});
var urlsWithDescriptions =
await source.search(
values['searchQuery']
as String);
if (urlsWithDescriptions
.isNotEmpty) {
var selectedUrls =
// ignore: use_build_context_synchronously
await showDialog<
List<
String>?>(
context: context,
builder:
(BuildContext
ctx) {
return UrlSelectionModal(
urlsWithDescriptions:
urlsWithDescriptions,
selectedByDefault:
false,
);
});
if (selectedUrls !=
null &&
selectedUrls
.isNotEmpty) {
var errors =
await appsProvider
.addAppsByURL(
selectedUrls);
if (errors.isEmpty) {
// ignore: use_build_context_synchronously
showError(
tr('importedX',
args: [
plural(
'app',
selectedUrls
.length)
]),
context);
} else {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder:
(BuildContext
ctx) {
return ImportErrorDialog(
urlsLength:
selectedUrls
.length,
errors:
errors);
});
}
}
} else {
throw ObtainiumError(
tr('noResults'));
}
}
}()
.catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
importInProgress = false;
});
});
}, },
child: Text( child: Text(
tr('searchX', args: [source.name]))) tr('searchX', args: [source.name])))
@@ -390,93 +382,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
onPressed: importInProgress onPressed: importInProgress
? null ? null
: () { : () {
() async { runMassSourceImport(source);
var values = await showDialog<
Map<String,
dynamic>?>(
context: context,
builder:
(BuildContext ctx) {
return GeneratedFormModal(
title: tr('importX',
args: [
source.name
]),
items:
source
.requiredArgs
.map(
(e) => [
GeneratedFormTextField(e,
label: e)
])
.toList(),
);
});
if (values != null) {
setState(() {
importInProgress = true;
});
var urlsWithDescriptions =
await source
.getUrlsWithDescriptions(
values.values
.map((e) =>
e.toString())
.toList());
var selectedUrls =
// ignore: use_build_context_synchronously
await showDialog<
List<String>?>(
context: context,
builder:
(BuildContext
ctx) {
return UrlSelectionModal(
urlsWithDescriptions:
urlsWithDescriptions);
});
if (selectedUrls != null) {
var errors =
await appsProvider
.addAppsByURL(
selectedUrls);
if (errors.isEmpty) {
// ignore: use_build_context_synchronously
showError(
tr('importedX',
args: [
plural(
'app',
selectedUrls
.length)
]),
context);
} else {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder:
(BuildContext
ctx) {
return ImportErrorDialog(
urlsLength:
selectedUrls
.length,
errors:
errors);
});
}
}
}
}()
.catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
importInProgress = false;
});
});
}, },
child: Text( child: Text(
tr('importX', args: [source.name]))) tr('importX', args: [source.name])))

View File

@@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart'; import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/logs_provider.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -223,6 +224,17 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
themeDropdown, themeDropdown,
height16, height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('useBlackTheme')),
Switch(
value: settingsProvider.useBlackTheme,
onChanged: (value) {
settingsProvider.useBlackTheme = value;
})
],
),
colourDropdown, colourDropdown,
height16, height16,
Row( Row(
@@ -262,6 +274,18 @@ class _SettingsPageState extends State<SettingsPage> {
}) })
], ],
), ),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('groupByCategory')),
Switch(
value: settingsProvider.groupByCategory,
onChanged: (value) {
settingsProvider.groupByCategory = value;
})
],
),
const Divider( const Divider(
height: 16, height: 16,
), ),
@@ -432,6 +456,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var settingsProvider = context.watch<SettingsProvider>(); var settingsProvider = context.watch<SettingsProvider>();
var appsProvider = context.watch<AppsProvider>();
storedValues = settingsProvider.categories.map((key, value) => MapEntry( storedValues = settingsProvider.categories.map((key, value) => MapEntry(
key, key,
MapEntry(value, MapEntry(value,
@@ -455,8 +480,9 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
if (!isBuilding) { if (!isBuilding) {
storedValues = storedValues =
values['categories'] as Map<String, MapEntry<int, bool>>; values['categories'] as Map<String, MapEntry<int, bool>>;
settingsProvider.categories = settingsProvider.setCategories(
storedValues.map((key, value) => MapEntry(key, value.key)); storedValues.map((key, value) => MapEntry(key, value.key)),
appsProvider: appsProvider);
if (widget.onSelected != null) { if (widget.onSelected != null) {
widget.onSelected!(storedValues.keys widget.onSelected!(storedValues.keys
.where((k) => storedValues[k]!.value) .where((k) => storedValues[k]!.value)

View File

@@ -34,6 +34,10 @@ class AppInMemory {
AppInfo? installedInfo; AppInfo? installedInfo;
AppInMemory(this.app, this.downloadProgress, this.installedInfo); AppInMemory(this.app, this.downloadProgress, this.installedInfo);
AppInMemory deepCopy() =>
AppInMemory(app.deepCopy(), downloadProgress, installedInfo);
String get name => app.overrideName ?? installedInfo?.name ?? app.finalName;
} }
class DownloadedApk { class DownloadedApk {
@@ -73,6 +77,18 @@ List<String> generateStandardVersionRegExStrings() {
List<String> standardVersionRegExStrings = List<String> standardVersionRegExStrings =
generateStandardVersionRegExStrings(); generateStandardVersionRegExStrings();
Set<String> findStandardFormatsForVersion(String version, bool strict) {
// If !strict, even a substring match is valid
Set<String> results = {};
for (var pattern in standardVersionRegExStrings) {
if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}')
.hasMatch(version)) {
results.add(pattern);
}
}
return results;
}
class AppsProvider with ChangeNotifier { class AppsProvider with ChangeNotifier {
// In memory App state (should always be kept in sync with local storage versions) // In memory App state (should always be kept in sync with local storage versions)
Map<String, AppInMemory> apps = {}; Map<String, AppInMemory> apps = {};
@@ -85,6 +101,8 @@ class AppsProvider with ChangeNotifier {
late Stream<FGBGType>? foregroundStream; late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription; late StreamSubscription<FGBGType>? foregroundSubscription;
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
AppsProvider() { AppsProvider() {
// Subscribe to changes in the app foreground status // Subscribe to changes in the app foreground status
foregroundStream = FGBGEvents.stream.asBroadcastStream(); foregroundStream = FGBGEvents.stream.asBroadcastStream();
@@ -147,18 +165,17 @@ class AppsProvider with ChangeNotifier {
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async { Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
NotificationsProvider? notificationsProvider = NotificationsProvider? notificationsProvider =
context?.read<NotificationsProvider>(); context?.read<NotificationsProvider>();
var notifId = DownloadNotification(app.name, 0).id; var notifId = DownloadNotification(app.finalName, 0).id;
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = 0; apps[app.id]!.downloadProgress = 0;
notifyListeners(); notifyListeners();
} }
try { try {
var fileName =
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk';
String downloadUrl = await SourceProvider() String downloadUrl = await SourceProvider()
.getSource(app.url) .getSource(app.url)
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]); .apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value);
var notif = DownloadNotification(app.name, 100); var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
var notif = DownloadNotification(app.finalName, 100);
notificationsProvider?.cancel(notif.id); notificationsProvider?.cancel(notif.id);
int? prevProg; int? prevProg;
File downloadedFile = File downloadedFile =
@@ -168,7 +185,7 @@ class AppsProvider with ChangeNotifier {
apps[app.id]!.downloadProgress = progress; apps[app.id]!.downloadProgress = progress;
notifyListeners(); notifyListeners();
} }
notif = DownloadNotification(app.name, prog ?? 100); notif = DownloadNotification(app.finalName, prog ?? 100);
if (prog != null && prevProg != prog) { if (prog != null && prevProg != prog) {
notificationsProvider?.notify(notif); notificationsProvider?.notify(notif);
} }
@@ -193,7 +210,7 @@ class AppsProvider with ChangeNotifier {
var originalAppId = app.id; var originalAppId = app.id;
app.id = newInfo.packageName; app.id = newInfo.packageName;
downloadedFile = downloadedFile.renameSync( downloadedFile = downloadedFile.renameSync(
'${downloadedFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk'); '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
if (apps[originalAppId] != null) { if (apps[originalAppId] != null) {
await removeApps([originalAppId]); await removeApps([originalAppId]);
await saveApps([app]); await saveApps([app]);
@@ -284,9 +301,10 @@ class AppsProvider with ChangeNotifier {
await intent.launch(); await intent.launch();
} }
Future<String?> confirmApkUrl(App app, BuildContext? context) async { Future<MapEntry<String, String>?> confirmApkUrl(
App app, BuildContext? context) async {
// If the App has more than one APK, the user should pick one (if context provided) // If the App has more than one APK, the user should pick one (if context provided)
String? apkUrl = app.apkUrls[app.preferredApkIndex]; MapEntry<String, String>? apkUrl = app.apkUrls[app.preferredApkIndex];
// get device supported architecture // get device supported architecture
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis; List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
@@ -309,14 +327,14 @@ class AppsProvider with ChangeNotifier {
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided) // If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
if (apkUrl != null && if (apkUrl != null &&
getHost(apkUrl) != getHost(app.url) && getHost(apkUrl.value) != getHost(app.url) &&
context != null) { context != null) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (await showDialog( if (await showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return APKOriginWarningDialog( return APKOriginWarningDialog(
sourceUrl: app.url, apkUrl: apkUrl!); sourceUrl: app.url, apkUrl: apkUrl!.value);
}) != }) !=
true) { true) {
apkUrl = null; apkUrl = null;
@@ -341,7 +359,7 @@ class AppsProvider with ChangeNotifier {
if (apps[id] == null) { if (apps[id] == null) {
throw ObtainiumError(tr('appNotFound')); throw ObtainiumError(tr('appNotFound'));
} }
String? apkUrl; MapEntry<String, String>? apkUrl;
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true; var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
if (!trackOnly) { if (!trackOnly) {
apkUrl = await confirmApkUrl(apps[id]!.app, context); apkUrl = await confirmApkUrl(apps[id]!.app, context);
@@ -472,94 +490,117 @@ class AppsProvider with ChangeNotifier {
return res; return res;
} }
// If the App says it is installed but installedInfo is null, set it to not installed bool isVersionDetectionPossible(AppInMemory? app) {
// If there is any other mismatch between installedInfo and installedVersion, try reconciling them intelligently return app?.app.additionalSettings['trackOnly'] != true &&
// If that fails, just set it to the actual version string (all we can do at that point) app?.app.additionalSettings['versionDetection'] !=
// Don't save changes, just return the object if changes were made (else null) 'releaseDateAsVersion' &&
app?.installedInfo?.versionName != null &&
app?.app.installedVersion != null &&
reconcileVersionDifferences(
app!.installedInfo!.versionName!, app.app.installedVersion!) !=
null;
}
// Given an App and it's on-device info...
// Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
var modded = false; var modded = false;
var trackOnly = app.additionalSettings['trackOnly'] == true; var trackOnly = app.additionalSettings['trackOnly'] == true;
var noVersionDetection = app.additionalSettings['versionDetection'] != var noVersionDetection = app.additionalSettings['versionDetection'] !=
'standardVersionDetection'; 'standardVersionDetection';
// 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.installedVersion = null; app.installedVersion = null;
modded = true; modded = true;
} else if (installedInfo?.versionName != null && } else if (installedInfo?.versionName != null &&
app.installedVersion == null) { app.installedVersion == null) {
// App says it's not installed but really is - set to installed and use real package versionName
app.installedVersion = installedInfo!.versionName; app.installedVersion = installedInfo!.versionName;
modded = true; modded = true;
} else if (installedInfo?.versionName != null && }
// SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL
if (installedInfo?.versionName != null &&
installedInfo!.versionName != app.installedVersion && installedInfo!.versionName != app.installedVersion &&
!noVersionDetection) { !noVersionDetection) {
String? correctedInstalledVersion = reconcileRealAndInternalVersions( // App's reported version and real version don't match (and it uses standard version detection)
// If they share a standard format (and are still different under it), update the reported version accordingly
var correctedInstalledVersion = reconcileVersionDifferences(
installedInfo.versionName!, app.installedVersion!); installedInfo.versionName!, app.installedVersion!);
if (correctedInstalledVersion != null) { if (correctedInstalledVersion?.key == false) {
app.installedVersion = correctedInstalledVersion; app.installedVersion = correctedInstalledVersion!.value;
modded = true; modded = true;
} }
} }
// THIRD, RECONCILE THE APP'S REPORTED INSTALLED AND LATEST VERSIONS
if (app.installedVersion != null && if (app.installedVersion != null &&
app.installedVersion != app.latestVersion && app.installedVersion != app.latestVersion &&
!noVersionDetection) { !noVersionDetection) {
app.installedVersion = reconcileRealAndInternalVersions( // App's reported installed and latest versions don't match (and it uses standard version detection)
app.installedVersion!, app.latestVersion, // If they share a standard format, make sure the App's reported installed version uses that format
matchMode: true) ?? var correctedInstalledVersion =
app.installedVersion; reconcileVersionDifferences(app.installedVersion!, app.latestVersion);
if (correctedInstalledVersion?.key == true) {
app.installedVersion = correctedInstalledVersion!.value;
modded = true;
}
}
// FOURTH, DISABLE VERSION DETECTION IF ENABLED AND THE REPORTED/REAL INSTALLED VERSIONS ARE NOT STANDARDIZED
if (installedInfo != null &&
app.additionalSettings['versionDetection'] ==
'standardVersionDetection' &&
!isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) {
app.additionalSettings['versionDetection'] = 'noVersionDetection';
logs.add('Could not reconcile version formats for: ${app.id}');
modded = true; modded = true;
} }
// if (app.installedVersion != null &&
// app.additionalSettings['versionDetection'] ==
// 'standardVersionDetection') {
// var correctedInstalledVersion =
// reconcileVersionDifferences(app.installedVersion!, app.latestVersion);
// if (correctedInstalledVersion == null) {
// app.additionalSettings['versionDetection'] = 'noVersionDetection';
// logs.add('Could not reconcile version formats for: ${app.id}');
// modded = true;
// }
// }
return modded ? app : null; return modded ? app : null;
} }
String? reconcileRealAndInternalVersions( MapEntry<bool, String>? reconcileVersionDifferences(
String realVersion, String internalVersion, String templateVersion, String comparisonVersion) {
{bool matchMode = false}) { // Returns null if the versions don't share a common standard format
// 1. If one or both of these can't be converted to a "standard" format, return null (leave as is) // Returns <true, comparisonVersion> if they share a common format and are equal
// 2. If both have a "standard" format under which they are equal, return null (leave as is) // Returns <false, templateVersion> if they share a common format but are not equal
// 3. If both have a "standard" format in common but are unequal, return realVersion (this means it was changed externally) // templateVersion must fully match a standard format, while comparisonVersion can have a substring match
// If in matchMode, the outcomes of rules 2 and 3 are reversed, and the "real" version is not matched strictly var templateVersionFormats =
// Matchmode to be used when comparing internal install version and internal latest version findStandardFormatsForVersion(templateVersion, true);
var comparisonVersionFormats =
bool doStringsMatchUnderRegEx( findStandardFormatsForVersion(comparisonVersion, false);
String pattern, String value1, String value2) {
var r = RegExp(pattern);
var m1 = r.firstMatch(value1);
var m2 = r.firstMatch(value2);
return m1 != null && m2 != null
? value1.substring(m1.start, m1.end) ==
value2.substring(m2.start, m2.end)
: false;
}
Set<String> findStandardFormatsForVersion(String version, bool strict) {
Set<String> results = {};
for (var pattern in standardVersionRegExStrings) {
if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}')
.hasMatch(version)) {
results.add(pattern);
}
}
return results;
}
var realStandardVersionFormats =
findStandardFormatsForVersion(realVersion, true);
var internalStandardVersionFormats =
findStandardFormatsForVersion(internalVersion, false);
var commonStandardFormats = var commonStandardFormats =
realStandardVersionFormats.intersection(internalStandardVersionFormats); templateVersionFormats.intersection(comparisonVersionFormats);
if (commonStandardFormats.isEmpty) { if (commonStandardFormats.isEmpty) {
return null; // Incompatible; no "enhanced detection" return null;
} }
for (String pattern in commonStandardFormats) { for (String pattern in commonStandardFormats) {
if (doStringsMatchUnderRegEx(pattern, internalVersion, realVersion)) { if (doStringsMatchUnderRegEx(
return matchMode pattern, comparisonVersion, templateVersion)) {
? internalVersion return MapEntry(true, comparisonVersion);
: null; // Enhanced detection says no change
} }
} }
return matchMode return MapEntry(false, templateVersion);
? null }
: realVersion; // Enhanced detection says something changed
bool doStringsMatchUnderRegEx(String pattern, String value1, String value2) {
var r = RegExp(pattern);
var m1 = r.firstMatch(value1);
var m2 = r.firstMatch(value2);
return m1 != null && m2 != null
? value1.substring(m1.start, m1.end) ==
value2.substring(m2.start, m2.end)
: false;
} }
Future<void> loadApps() async { Future<void> loadApps() async {
@@ -602,7 +643,7 @@ class AppsProvider with ChangeNotifier {
sp.getSource(newApps[i].url); sp.getSource(newApps[i].url);
apps[newApps[i].id] = AppInMemory(newApps[i], null, info); apps[newApps[i].id] = AppInMemory(newApps[i], null, info);
} catch (e) { } catch (e) {
errors.add([newApps[i].id, newApps[i].name, e.toString()]); errors.add([newApps[i].id, newApps[i].finalName, e.toString()]);
} }
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
@@ -632,7 +673,8 @@ class AppsProvider with ChangeNotifier {
bool onlyIfExists = true}) async { bool onlyIfExists = true}) async {
attemptToCorrectInstallStatus = attemptToCorrectInstallStatus =
attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork()); attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork());
for (var app in apps) { for (var a in apps) {
var app = a.deepCopy();
AppInfo? info = await getInstalledInfo(app.id); AppInfo? info = await getInstalledInfo(app.id);
app.name = info?.name ?? app.name; app.name = info?.name ?? app.name;
if (attemptToCorrectInstallStatus) { if (attemptToCorrectInstallStatus) {
@@ -669,8 +711,11 @@ class AppsProvider with ChangeNotifier {
} }
Future<bool> removeAppsWithModal(BuildContext context, List<App> apps) async { Future<bool> removeAppsWithModal(BuildContext context, List<App> apps) async {
var showUninstallOption = var showUninstallOption = apps
apps.where((a) => a.installedVersion != null).isNotEmpty; .where((a) =>
a.installedVersion != null &&
a.additionalSettings['trackOnly'] != true)
.isNotEmpty;
var values = await showDialog( var values = await showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
@@ -719,6 +764,18 @@ class AppsProvider with ChangeNotifier {
await intent.launch(); await intent.launch();
} }
addMissingCategories(SettingsProvider settingsProvider) {
var cats = settingsProvider.categories;
apps.forEach((key, value) {
for (var c in value.app.categories) {
if (!cats.containsKey(c)) {
cats[c] = generateRandomLightColor().value;
}
}
});
settingsProvider.setCategories(cats, appsProvider: this);
}
Future<App?> checkUpdate(String appId) async { Future<App?> checkUpdate(String appId) async {
App? currentApp = apps[appId]!.app; App? currentApp = apps[appId]!.app;
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
@@ -798,12 +855,6 @@ class AppsProvider with ChangeNotifier {
} }
Future<String> exportApps() async { Future<String> exportApps() async {
Directory? exportDir = Directory('/storage/emulated/0/Download');
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
if (!exportDir.existsSync()) {
exportDir = await getExternalStorageDirectory();
path = exportDir!.path;
}
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
if (await Permission.storage.isDenied) { if (await Permission.storage.isDenied) {
await Permission.storage.request(); await Permission.storage.request();
@@ -812,6 +863,18 @@ class AppsProvider with ChangeNotifier {
throw ObtainiumError(tr('storagePermissionDenied')); throw ObtainiumError(tr('storagePermissionDenied'));
} }
} }
Directory? exportDir = Directory('/storage/emulated/0/Download');
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
var downloadsAccessible = false;
try {
downloadsAccessible = exportDir.existsSync();
} catch (e) {
logs.add('Error accessing Downloads (will use fallback): $e');
}
if (!downloadsAccessible) {
exportDir = await getExternalStorageDirectory();
path = exportDir!.path;
}
File export = File( File export = File(
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync( export.writeAsStringSync(
@@ -864,7 +927,7 @@ class APKPicker extends StatefulWidget {
const APKPicker({super.key, required this.app, this.initVal, this.archs}); const APKPicker({super.key, required this.app, this.initVal, this.archs});
final App app; final App app;
final String? initVal; final MapEntry<String, String>? initVal;
final List<String>? archs; final List<String>? archs;
@override @override
@@ -872,7 +935,7 @@ class APKPicker extends StatefulWidget {
} }
class _APKPickerState extends State<APKPicker> { class _APKPickerState extends State<APKPicker> {
String? apkUrl; MapEntry<String, String>? apkUrl;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -881,19 +944,17 @@ class _APKPickerState extends State<APKPicker> {
scrollable: true, scrollable: true,
title: Text(tr('pickAnAPK')), title: Text(tr('pickAnAPK')),
content: Column(children: [ content: Column(children: [
Text(tr('appHasMoreThanOnePackage', args: [widget.app.name])), Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])),
const SizedBox(height: 16), const SizedBox(height: 16),
...widget.app.apkUrls.map( ...widget.app.apkUrls.map(
(u) => RadioListTile<String>( (u) => RadioListTile<String>(
title: Text(Uri.parse(u) title: Text(u.key),
.pathSegments value: u.value,
.where((element) => element.isNotEmpty) groupValue: apkUrl!.value,
.last),
value: u,
groupValue: apkUrl,
onChanged: (String? val) { onChanged: (String? val) {
setState(() { setState(() {
apkUrl = val; apkUrl =
widget.app.apkUrls.where((e) => e.value == val).first;
}); });
}), }),
), ),

View File

@@ -34,9 +34,9 @@ class UpdateNotification extends ObtainiumNotification {
message = updates.isEmpty message = updates.isEmpty
? tr('noNewUpdates') ? tr('noNewUpdates')
: updates.length == 1 : updates.length == 1
? tr('xHasAnUpdate', args: [updates[0].name]) ? tr('xHasAnUpdate', args: [updates[0].finalName])
: plural('xAndNMoreUpdatesAvailable', updates.length - 1, : plural('xAndNMoreUpdatesAvailable', updates.length - 1,
args: [updates[0].name, (updates.length - 1).toString()]); args: [updates[0].finalName, (updates.length - 1).toString()]);
} }
} }
@@ -46,9 +46,9 @@ class SilentUpdateNotification extends ObtainiumNotification {
tr('appsUpdatedNotifDescription'), Importance.defaultImportance) { tr('appsUpdatedNotifDescription'), Importance.defaultImportance) {
message = updates.length == 1 message = updates.length == 1
? tr('xWasUpdatedToY', ? tr('xWasUpdatedToY',
args: [updates[0].name, updates[0].latestVersion]) args: [updates[0].finalName, updates[0].latestVersion])
: plural('xAndNMoreUpdatesInstalled', updates.length - 1, : plural('xAndNMoreUpdatesInstalled', updates.length - 1,
args: [updates[0].name, (updates.length - 1).toString()]); args: [updates[0].finalName, (updates.length - 1).toString()]);
} }
} }

View File

@@ -7,6 +7,8 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/main.dart'; import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_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:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -62,6 +64,15 @@ class SettingsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool get useBlackTheme {
return prefs?.getBool('useBlackTheme') ?? false;
}
set useBlackTheme(bool useBlackTheme) {
prefs?.setBool('useBlackTheme', useBlackTheme);
notifyListeners();
}
int get updateInterval { int get updateInterval {
var min = prefs?.getInt('updateInterval') ?? 360; var min = prefs?.getInt('updateInterval') ?? 360;
if (!updateIntervals.contains(min)) { if (!updateIntervals.contains(min)) {
@@ -139,6 +150,15 @@ class SettingsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool get groupByCategory {
return prefs?.getBool('groupByCategory') ?? false;
}
set groupByCategory(bool show) {
prefs?.setBool('groupByCategory', show);
notifyListeners();
}
String? getSettingString(String settingId) { String? getSettingString(String settingId) {
return prefs?.getString(settingId); return prefs?.getString(settingId);
} }
@@ -151,7 +171,23 @@ class SettingsProvider with ChangeNotifier {
Map<String, int> get categories => Map<String, int> get categories =>
Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}')); Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}'));
set categories(Map<String, int> cats) { void setCategories(Map<String, int> cats, {AppsProvider? appsProvider}) {
if (appsProvider != null) {
List<App> changedApps = appsProvider
.getAppValues()
.map((a) {
var n1 = a.app.categories.length;
a.app.categories.removeWhere((c) => !cats.keys.contains(c));
return n1 > a.app.categories.length ? a.app : null;
})
.where((element) => element != null)
.map((e) => e as App)
.toList();
if (changedApps.isNotEmpty) {
appsProvider.saveApps(changedApps,
attemptToCorrectInstallStatus: false);
}
}
prefs?.setString('categories', jsonEncode(cats)); prefs?.setString('categories', jsonEncode(cats));
notifyListeners(); notifyListeners();
} }

View File

@@ -3,6 +3,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@@ -21,7 +22,6 @@ import 'package:obtainium/app_sources/sourceforge.dart';
import 'package:obtainium/app_sources/steammobile.dart'; import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/app_sources/telegramapp.dart'; import 'package:obtainium/app_sources/telegramapp.dart';
import 'package:obtainium/app_sources/vlc.dart'; import 'package:obtainium/app_sources/vlc.dart';
import 'package:obtainium/app_sources/whatsapp.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/mass_app_sources/githubstars.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart';
@@ -35,7 +35,7 @@ class AppNames {
class APKDetails { class APKDetails {
late String version; late String version;
late List<String> apkUrls; late List<MapEntry<String, String>> apkUrls;
late AppNames names; late AppNames names;
late DateTime? releaseDate; late DateTime? releaseDate;
late String? changeLog; late String? changeLog;
@@ -51,7 +51,7 @@ class App {
late String name; late String name;
String? installedVersion; String? installedVersion;
late String latestVersion; late String latestVersion;
List<String> apkUrls = []; List<MapEntry<String, String>> apkUrls = [];
late int preferredApkIndex; late int preferredApkIndex;
late Map<String, dynamic> additionalSettings; late Map<String, dynamic> additionalSettings;
late DateTime? lastUpdateCheck; late DateTime? lastUpdateCheck;
@@ -80,6 +80,31 @@ class App {
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned'; return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
} }
String? get overrideName =>
additionalSettings['appName']?.toString().trim().isNotEmpty == true
? additionalSettings['appName']
: null;
String get finalName {
return overrideName ?? name;
}
App deepCopy() => App(
id,
url,
author,
name,
installedVersion,
latestVersion,
apkUrls,
preferredApkIndex,
Map.from(additionalSettings),
lastUpdateCheck,
pinned,
categories: categories,
changeLog: changeLog,
releaseDate: releaseDate);
factory App.fromJson(Map<String, dynamic> json) { factory App.fromJson(Map<String, dynamic> json) {
var source = SourceProvider().getSource(json['url']); var source = SourceProvider().getSource(json['url']);
var formItems = source.combinedAppSpecificSettingFormItems var formItems = source.combinedAppSpecificSettingFormItems
@@ -111,16 +136,16 @@ class App {
// Convert bool style version detection options to dropdown style // Convert bool style version detection options to dropdown style
if (additionalSettings['noVersionDetection'] == true) { if (additionalSettings['noVersionDetection'] == true) {
additionalSettings['versionDetection'] = 'noVersionDetection'; additionalSettings['versionDetection'] = 'noVersionDetection';
} if (additionalSettings['releaseDateAsVersion'] == true) {
if (additionalSettings['releaseDateAsVersion'] == true) { additionalSettings['versionDetection'] = 'releaseDateAsVersion';
additionalSettings['versionDetection'] = 'releaseDateAsVersion'; additionalSettings.remove('releaseDateAsVersion');
additionalSettings.remove('releaseDateAsVersion'); }
} if (additionalSettings['noVersionDetection'] != null) {
if (additionalSettings['noVersionDetection'] != null) { additionalSettings.remove('noVersionDetection');
additionalSettings.remove('noVersionDetection'); }
} if (additionalSettings['releaseDateAsVersion'] != null) {
if (additionalSettings['releaseDateAsVersion'] != null) { additionalSettings.remove('releaseDateAsVersion');
additionalSettings.remove('releaseDateAsVersion'); }
} }
// Ensure additionalSettings are correctly typed // Ensure additionalSettings are correctly typed
for (var item in formItems) { for (var item in formItems) {
@@ -135,6 +160,23 @@ class App {
if (preferredApkIndex < 0) { if (preferredApkIndex < 0) {
preferredApkIndex = 0; preferredApkIndex = 0;
} }
// apkUrls can either be old list or new named list apkUrls
List<MapEntry<String, String>> apkUrls = [];
if (json['apkUrls'] != null) {
var apkUrlJson = jsonDecode(json['apkUrls']);
try {
apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson));
} catch (e) {
apkUrls = List<dynamic>.from(apkUrlJson)
.map((e) => MapEntry(e[0] as String, e[1] as String))
.toList();
}
}
// Arch based APK filter option should be disabled if it previously did not exist
if (json['additionalSettings'] != null &&
jsonDecode(json['additionalSettings'])['autoApkFilterByArch'] == null) {
additionalSettings['autoApkFilterByArch'] = false;
}
return App( return App(
json['id'] as String, json['id'] as String,
json['url'] as String, json['url'] as String,
@@ -144,9 +186,7 @@ class App {
? null ? null
: json['installedVersion'] as String, : json['installedVersion'] as String,
json['latestVersion'] as String, json['latestVersion'] as String,
json['apkUrls'] == null apkUrls,
? []
: List<String>.from(jsonDecode(json['apkUrls'])),
preferredApkIndex, preferredApkIndex,
additionalSettings, additionalSettings,
json['lastUpdateCheck'] == null json['lastUpdateCheck'] == null
@@ -174,7 +214,7 @@ class App {
'name': name, 'name': name,
'installedVersion': installedVersion, 'installedVersion': installedVersion,
'latestVersion': latestVersion, 'latestVersion': latestVersion,
'apkUrls': jsonEncode(apkUrls), 'apkUrls': jsonEncode(apkUrls.map((e) => [e.key, e.value]).toList()),
'preferredApkIndex': preferredApkIndex, 'preferredApkIndex': preferredApkIndex,
'additionalSettings': jsonEncode(additionalSettings), 'additionalSettings': jsonEncode(additionalSettings),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
@@ -226,6 +266,13 @@ Map<String, dynamic> getDefaultValuesFromFormItems(
.reduce((value, element) => [...value, ...element])); .reduce((value, element) => [...value, ...element]));
} }
List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
urls.map((e) {
var segments = e.split('/').where((el) => el.trim().isNotEmpty);
var apkSegs = segments.where((s) => s.toLowerCase().endsWith('.apk'));
return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
}).toList();
class AppSource { class AppSource {
String? host; String? host;
late String name; late String name;
@@ -279,7 +326,12 @@ class AppSource {
return regExValidator(value); return regExValidator(value);
} }
]) ])
] ],
[
GeneratedFormSwitch('autoApkFilterByArch',
label: tr('autoApkFilterByArch'), defaultValue: true)
],
[GeneratedFormTextField('appName', label: tr('appName'), required: false)]
]; ];
// Previous 2 variables combined into one at runtime for convenient usage // Previous 2 variables combined into one at runtime for convenient usage
@@ -363,7 +415,7 @@ class SourceProvider {
url = preStandardizeUrl(url); url = preStandardizeUrl(url);
AppSource? source; AppSource? source;
for (var s in sources.where((element) => element.host != null)) { for (var s in sources.where((element) => element.host != null)) {
if (url.contains('://${s.host}')) { if (RegExp('://(.+\\.)?${s.host}').hasMatch(url)) {
source = s; source = s;
break; break;
} }
@@ -422,14 +474,29 @@ class SourceProvider {
if (additionalSettings['apkFilterRegEx'] != null) { if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']); var reg = RegExp(additionalSettings['apkFilterRegEx']);
apk.apkUrls = apk.apkUrls =
apk.apkUrls.where((element) => reg.hasMatch(element)).toList(); apk.apkUrls.where((element) => reg.hasMatch(element.key)).toList();
} }
if (apk.apkUrls.isEmpty && !trackOnly) { if (apk.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError(); throw NoAPKError();
} }
if (apk.apkUrls.length > 1 &&
additionalSettings['autoApkFilterByArch'] == true) {
var abis = (await DeviceInfoPlugin().androidInfo).supportedAbis;
for (var abi in abis) {
var urls2 = apk.apkUrls
.where((element) => RegExp('.*$abi.*').hasMatch(element.key))
.toList();
if (urls2.isNotEmpty && urls2.length < apk.apkUrls.length) {
apk.apkUrls = urls2;
break;
}
}
}
String apkVersion = apk.version.replaceAll('/', '-'); String apkVersion = apk.version.replaceAll('/', '-');
var name = currentApp?.name.trim() ?? var name = currentApp != null ? currentApp.name.trim() : '';
apk.names.name[0].toUpperCase() + apk.names.name.substring(1); name = name.isNotEmpty
? name
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
return App( return App(
currentApp?.id ?? currentApp?.id ??
source.tryInferringAppId(standardUrl, source.tryInferringAppId(standardUrl,
@@ -437,9 +504,7 @@ class SourceProvider {
generateTempID(standardUrl, additionalSettings), generateTempID(standardUrl, additionalSettings),
standardUrl, standardUrl,
apk.names.author[0].toUpperCase() + apk.names.author.substring(1), apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
name.trim().isNotEmpty name,
? name
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1),
currentApp?.installedVersion, currentApp?.installedVersion,
apkVersion, apkVersion,
apk.apkUrls, apk.apkUrls,

View File

@@ -5,18 +5,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: android_alarm_manager_plus name: android_alarm_manager_plus
sha256: "8647cc5f9339f3955a2bd9ec40e0f10c3a80049f31f80b3ffdd87e07bb73fce2" sha256: f6d0347734fa2ea716349a5a3e16ffdc1800ca64e5640112896d128c6815c178
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
android_intent_plus: android_intent_plus:
dependency: "direct main" dependency: "direct main"
description: description:
name: android_intent_plus name: android_intent_plus
sha256: "54810cb33945c2c10742cd746ea994822c115e9dbe189919bc63cb436e45a6af" sha256: "6bcdcd20461ac7a0c785f6298cdda96ad275d5bcbc1ecf28829cbe03ec6690be"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.6" version: "3.1.7"
animations: animations:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -117,10 +117,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.0" version: "8.2.0"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -133,10 +133,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dynamic_color name: dynamic_color
sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.2" version: "1.6.3"
easy_localization: easy_localization:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -181,10 +181,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: d8e9ca7e5d1983365c277f12c21b4362df6cf659c99af146ad4d04eb33033013 sha256: dcde5ad1a0cebcf3715ea3f24d0db1888bf77027a26c77d7779e8ef63b8ade62
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.6" version: "5.2.9"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -337,10 +337,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: markdown name: markdown
sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.2"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -409,10 +409,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.13" version: "2.0.14"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
@@ -425,10 +425,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "12eee51abdf4d34c590f043f45073adbb45514a108bd9db4491547a2fd891059" sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.2"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -473,10 +473,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_apple name: permission_handler_apple
sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.7" version: "9.0.8"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -537,74 +537,74 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625" sha256: "692261968a494e47323dcc8bc66d8d52e81bc27cb4b808e4e8d7e8079d4cc01a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.1" version: "6.3.2"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: share_plus_platform_interface name: share_plus_platform_interface
sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1" sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.18" version: "2.1.0"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521 sha256: "1bc5d734b109c327b922b0891b41fc51483ccbd53c0d19952b7e230349a5d90b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.17" version: "2.1.1"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310" sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.2.1"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707" sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.2.0"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.2.0"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8" sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.6" version: "2.1.0"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436" sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.2.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -710,18 +710,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.25" version: "6.0.27"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.3" version: "6.1.4"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@@ -734,10 +734,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.4" version: "3.0.5"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -790,34 +790,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: "34f83c2f0f64c75ad75c77a2ccfc8d2e531afbe8ad41af1fd787d6d33336aa90" sha256: bdd3ddbeaf341c75620e153d22b6f4f6c4a342fe4439ed3a10db74dd82e65d1c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.3" version: "3.5.1"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
sha256: "1939c39e2150fb4d30fd3cc59a891a49fed9935db53007df633ed83581b6117b" sha256: "6341f92977609be71391f4d4dcd64bfaa8ac657af1dfb2e231b5c1724e8c6c36"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.2.0"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: ab12479f7a0cf112b9420c36aaf206a1ca47cd60cd42de74a4be2e97a697587b sha256: "2ef3f65fd49061c18e4d837a411308f2850417f2d0a7c11aad2c3857bee12c18"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.3.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.4"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -835,5 +835,5 @@ packages:
source: hosted source: hosted
version: "6.2.2" version: "6.2.2"
sdks: sdks:
dart: ">=2.18.2 <3.0.0" dart: ">=2.19.0 <3.0.0"
flutter: ">=3.4.0-17.0.pre" flutter: ">=3.4.0-17.0.pre"

View File

@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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.11.14+135 # When changing this, update the tag in main() accordingly version: 0.11.32+154 # When changing this, update the tag in main() accordingly
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.18.2 <3.0.0'
@@ -92,6 +92,7 @@ flutter:
assets: assets:
- assets/translations/ - assets/translations/
- assets/graphics/ - assets/graphics/
- assets/ca/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware