mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			v0.11.33-b
			...
			v0.12.0-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					08aa04f812 | ||
| 
						 | 
					dd19fcf6da | ||
| 
						 | 
					04b3c8ad7d | ||
| 
						 | 
					81f66683d2 | ||
| 
						 | 
					392554123b | ||
| 
						 | 
					3e4d5c26ac | ||
| 
						 | 
					86b7f6fef3 | ||
| 
						 | 
					e1d914118f | ||
| 
						 | 
					4a07cf9951 | ||
| 
						 | 
					ce44e200a5 | ||
| 
						 | 
					e8ebf53626 | ||
| 
						 | 
					cdd6a4124c | ||
| 
						 | 
					09c71e4e9f | ||
| 
						 | 
					28a996441c | ||
| 
						 | 
					396bf012c9 | ||
| 
						 | 
					02da24aa75 | ||
| 
						 | 
					3c6e66ce12 | 
@@ -17,7 +17,6 @@ Currently supported App sources:
 | 
			
		||||
- [SourceForge](https://sourceforge.net/)
 | 
			
		||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
 | 
			
		||||
- Third Party F-Droid Repos
 | 
			
		||||
  - Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
 | 
			
		||||
- [Steam](https://store.steampowered.com/mobile)
 | 
			
		||||
- [Telegram App](https://telegram.org)
 | 
			
		||||
- [VLC](https://www.videolan.org/vlc/download-android.html)
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
 | 
			
		||||
    "remove": "Entfernen",
 | 
			
		||||
    "yesMarkUpdated": "Ja, als aktualisiert markieren",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "App ID oder Name",
 | 
			
		||||
    "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
 | 
			
		||||
    "reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
 | 
			
		||||
@@ -224,6 +224,10 @@
 | 
			
		||||
    "standardVersionDetection": "Standardversionserkennung",
 | 
			
		||||
    "groupByCategory": "Nach Kategorie gruppieren",
 | 
			
		||||
    "autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "App entfernen?",
 | 
			
		||||
        "other": "Apps entfernen?"
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "Last Update Check: {}",
 | 
			
		||||
    "remove": "Remove",
 | 
			
		||||
    "yesMarkUpdated": "Yes, Mark as Updated",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "App ID or Name",
 | 
			
		||||
    "appWithIdOrNameNotFound": "No App was found with that ID or Name",
 | 
			
		||||
    "reposHaveMultipleApps": "Repos may contain multiple Apps",
 | 
			
		||||
@@ -224,6 +224,10 @@
 | 
			
		||||
    "standardVersionDetection": "Standard version detection",
 | 
			
		||||
    "groupByCategory": "Group by Category",
 | 
			
		||||
    "autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remove App?",
 | 
			
		||||
        "other": "Remove Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "بررسی آخرین بهروزرسانی: {}",
 | 
			
		||||
    "remove": "حذف",
 | 
			
		||||
    "yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "شناسه یا نام برنامه",
 | 
			
		||||
    "appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
 | 
			
		||||
    "reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
 | 
			
		||||
@@ -224,6 +224,10 @@
 | 
			
		||||
    "standardVersionDetection": "تشخیص نسخه استاندارد",
 | 
			
		||||
    "groupByCategory": "گروه بر اساس دسته",
 | 
			
		||||
    "autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "برنامه حذف شود؟",
 | 
			
		||||
        "other": "برنامه ها حذف شوند؟"
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "Vérification de la dernière mise à jour : {}",
 | 
			
		||||
    "remove": "Retirer",
 | 
			
		||||
    "yesMarkUpdated": "Oui, marquer comme mis à jour",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "ID ou nom de l'application",
 | 
			
		||||
    "appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
 | 
			
		||||
    "reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
 | 
			
		||||
@@ -224,6 +224,10 @@
 | 
			
		||||
    "standardVersionDetection": "Détection de version standard",
 | 
			
		||||
    "groupByCategory": "Group by Category",
 | 
			
		||||
    "autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Supprimer l'application ?",
 | 
			
		||||
        "other": "Supprimer les applications ?"
 | 
			
		||||
 
 | 
			
		||||
@@ -122,7 +122,7 @@
 | 
			
		||||
    "followSystem": "Rendszer szerint",
 | 
			
		||||
    "obtainium": "Obtainium",
 | 
			
		||||
    "materialYou": "Material You",
 | 
			
		||||
    "useBlackTheme": "Use pure black dark theme",
 | 
			
		||||
    "useBlackTheme": "Használjon tiszta fekete sötét témát",
 | 
			
		||||
    "appSortBy": "App rendezés...",
 | 
			
		||||
    "authorName": "Szerző/Név",
 | 
			
		||||
    "nameAuthor": "Név/Szerző",
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "Frissítés ellenőrizve: {}",
 | 
			
		||||
    "remove": "Eltávolítás",
 | 
			
		||||
    "yesMarkUpdated": "Igen, megjelölés frissítettként",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "App ID vagy név",
 | 
			
		||||
    "appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
 | 
			
		||||
    "reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
 | 
			
		||||
@@ -207,7 +207,7 @@
 | 
			
		||||
    "addCategory": "Új kategória",
 | 
			
		||||
    "label": "Címke",
 | 
			
		||||
    "language": "Nyelv",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "copiedToClipboard": "Másolva a vágólapra",
 | 
			
		||||
    "storagePermissionDenied": "Tárhely engedély megtagadva",
 | 
			
		||||
    "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",
 | 
			
		||||
@@ -223,6 +223,10 @@
 | 
			
		||||
    "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",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Eltávolítja az alkalmazást?",
 | 
			
		||||
        "other": "Eltávolítja az alkalmazást?"
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
 | 
			
		||||
    "remove": "Rimuovi",
 | 
			
		||||
    "yesMarkUpdated": "Sì, contrassegna come aggiornato",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "ID o nome dell'App",
 | 
			
		||||
    "appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
 | 
			
		||||
    "reposHaveMultipleApps": "I repository possono contenere più App",
 | 
			
		||||
@@ -224,6 +224,10 @@
 | 
			
		||||
    "standardVersionDetection": "Rilevamento di versione standard",
 | 
			
		||||
    "groupByCategory": "Raggruppa per categoria",
 | 
			
		||||
    "autoApkFilterByArch": "Tenta di filtrare gli APK in base all'architettura della CPU, se possibile",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Rimuovere l'App?",
 | 
			
		||||
        "other": "Rimuovere le App?"
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@
 | 
			
		||||
    "lastUpdateCheckX": "最終アップデート確認: {}",
 | 
			
		||||
    "remove": "削除",
 | 
			
		||||
    "yesMarkUpdated": "はい、アップデート済みとしてマークします",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "アプリのIDまたは名前",
 | 
			
		||||
    "appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
 | 
			
		||||
    "reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
 | 
			
		||||
@@ -224,6 +224,10 @@
 | 
			
		||||
    "standardVersionDetection": "標準のバージョン検出",
 | 
			
		||||
    "groupByCategory": "カテゴリ別にグループ化する",
 | 
			
		||||
    "autoApkFilterByArch": "可能であれば,CPUアーキテクチャによるAPKのフィルタリングを試みる",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "アプリを削除しますか?",
 | 
			
		||||
        "other": "アプリを削除しますか?"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,106 +1,105 @@
 | 
			
		||||
{
 | 
			
		||||
    "invalidURLForSource": "不是一个有效的 {} URL",
 | 
			
		||||
    "noReleaseFound": "找不到合适的更新",
 | 
			
		||||
    "noVersionFound": "无法确定更新版本",
 | 
			
		||||
    "urlMatchesNoSource": "URL 与已知来源不符",
 | 
			
		||||
    "cantInstallOlderVersion": "无法安装旧版应用程序",
 | 
			
		||||
    "appIdMismatch": "下载的软件包名与现有的应用程序包名不一致",
 | 
			
		||||
    "functionNotImplemented": "该类没有实现此功能",
 | 
			
		||||
    "invalidURLForSource": "无效的 {} URL",
 | 
			
		||||
    "noReleaseFound": "找不到合适的发行版",
 | 
			
		||||
    "noVersionFound": "无法确定发行版本号",
 | 
			
		||||
    "urlMatchesNoSource": "URL 与已知的来源不符",
 | 
			
		||||
    "cantInstallOlderVersion": "无法安装旧版本的应用",
 | 
			
		||||
    "appIdMismatch": "所下载 APK 的应用 ID 与现有应用不一致",
 | 
			
		||||
    "functionNotImplemented": "该类未实现此功能",
 | 
			
		||||
    "placeholder": "占位符",
 | 
			
		||||
    "someErrors": "出现了一些错误",
 | 
			
		||||
    "unexpectedError": "意外错误",
 | 
			
		||||
    "ok": "好的",
 | 
			
		||||
    "and": "和",
 | 
			
		||||
    "startedBgUpdateTask": "开始后台检查更新任务",
 | 
			
		||||
    "bgUpdateIgnoreAfterIs": "下次后台更新检查  {}",
 | 
			
		||||
    "startedActualBGUpdateCheck": "后台检查更新已开始",
 | 
			
		||||
    "bgUpdateTaskFinished": "后台检查更新已完成",
 | 
			
		||||
    "firstRun": "这是你第一次运行 Obtainium",
 | 
			
		||||
    "settingUpdateCheckIntervalTo": "设置检查更新间隔为 {}",
 | 
			
		||||
    "githubPATLabel": "GitHub 个人访问令牌 (提高 API 限制)",
 | 
			
		||||
    "githubPATHint": "个人访问令牌必须为: username:token 形式",
 | 
			
		||||
    "startedBgUpdateTask": "后台更新检查任务已启动",
 | 
			
		||||
    "bgUpdateIgnoreAfterIs": "后台更新检查间隔为 {}",
 | 
			
		||||
    "startedActualBGUpdateCheck": "开始后台更新检查",
 | 
			
		||||
    "bgUpdateTaskFinished": "后台更新检查任务已完成",
 | 
			
		||||
    "firstRun": "这是 Obtainium 首次启动",
 | 
			
		||||
    "settingUpdateCheckIntervalTo": "更新检查间隔设置为 {}",
 | 
			
		||||
    "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
 | 
			
		||||
    "githubPATHint": "个人访问令牌必须为“username:token”的格式",
 | 
			
		||||
    "githubPATFormat": "username:token",
 | 
			
		||||
    "githubPATLinkText": "关于 GitHub 个人访问令牌",
 | 
			
		||||
    "includePrereleases": "包含预发布版",
 | 
			
		||||
    "fallbackToOlderReleases": "回退到旧版",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "使用正则以过滤发布标题",
 | 
			
		||||
    "invalidRegEx": "表达式无效",
 | 
			
		||||
    "includePrereleases": "包含预发行版",
 | 
			
		||||
    "fallbackToOlderReleases": "将旧发行版作为备选",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
 | 
			
		||||
    "invalidRegEx": "无效的正则表达式",
 | 
			
		||||
    "noDescription": "无描述",
 | 
			
		||||
    "cancel": "取消",
 | 
			
		||||
    "continue": "继续",
 | 
			
		||||
    "requiredInBrackets": "(必须)",
 | 
			
		||||
    "dropdownNoOptsError": "错误:下拉菜单必须至少有一个选项",
 | 
			
		||||
    "colour": "颜色",
 | 
			
		||||
    "requiredInBrackets": "(必填)",
 | 
			
		||||
    "dropdownNoOptsError": "错误:下拉菜单必须包含至少一个选项",
 | 
			
		||||
    "colour": "配色",
 | 
			
		||||
    "githubStarredRepos": "GitHub 已星标仓库",
 | 
			
		||||
    "uname": "用户名",
 | 
			
		||||
    "wrongArgNum": "提供了错误的参数数量",
 | 
			
		||||
    "xIsTrackOnly": "{} 仅追踪",
 | 
			
		||||
    "source": "源码",
 | 
			
		||||
    "app": "应用程序",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "来自此来源的应用为仅追踪",
 | 
			
		||||
    "youPickedTrackOnly": "你已选择仅追踪选项",
 | 
			
		||||
    "trackOnlyAppDescription": "该应用程序将被跟踪更新,但 Obtainium 无法下载或安装它",
 | 
			
		||||
    "wrongArgNum": "参数数量错误",
 | 
			
		||||
    "xIsTrackOnly": "{} 为“仅追踪”模式",
 | 
			
		||||
    "source": "源代码",
 | 
			
		||||
    "app": "应用",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
 | 
			
		||||
    "youPickedTrackOnly": "您选择了“仅追踪”。",
 | 
			
		||||
    "trackOnlyAppDescription": "该应用的更新会被追踪,但 Obtainium 无法下载或安装它。",
 | 
			
		||||
    "cancelled": "已取消",
 | 
			
		||||
    "appAlreadyAdded": "此应用程序已被添加",
 | 
			
		||||
    "alreadyUpToDateQuestion": "应用已是最新?",
 | 
			
		||||
    "appAlreadyAdded": "此应用已经添加",
 | 
			
		||||
    "alreadyUpToDateQuestion": "应用是否已经为最新版本?",
 | 
			
		||||
    "addApp": "添加应用",
 | 
			
		||||
    "appSourceURL": "应用来源 URL",
 | 
			
		||||
    "appSourceURL": "来源 URL",
 | 
			
		||||
    "error": "错误",
 | 
			
		||||
    "add": "添加",
 | 
			
		||||
    "searchSomeSourcesLabel": "搜索 (仅部分来源)",
 | 
			
		||||
    "searchSomeSourcesLabel": "搜索(仅部分来源)",
 | 
			
		||||
    "search": "搜索",
 | 
			
		||||
    "additionalOptsFor": "{} 的更多选项",
 | 
			
		||||
    "supportedSourcesBelow": "受支持的来源:",
 | 
			
		||||
    "trackOnlyInBrackets": "(仅追踪)",
 | 
			
		||||
    "searchableInBrackets": "(可被搜索)",
 | 
			
		||||
    "appsString": "应用程序",
 | 
			
		||||
    "noApps": "无应用程序",
 | 
			
		||||
    "noAppsForFilter": "没有应用可被过滤",
 | 
			
		||||
    "byX": "来自 {}",
 | 
			
		||||
    "percentProgress": "进度: {}%",
 | 
			
		||||
    "pleaseWait": "请等待...",
 | 
			
		||||
    "supportedSourcesBelow": "支持的来源:",
 | 
			
		||||
    "trackOnlyInBrackets": "(仅追踪)",
 | 
			
		||||
    "searchableInBrackets": "(可搜索)",
 | 
			
		||||
    "appsString": "应用列表",
 | 
			
		||||
    "noApps": "无应用",
 | 
			
		||||
    "noAppsForFilter": "没有符合条件的应用",
 | 
			
		||||
    "byX": "作者:{}",
 | 
			
		||||
    "percentProgress": "进度:{}%",
 | 
			
		||||
    "pleaseWait": "请稍候",
 | 
			
		||||
    "updateAvailable": "更新可用",
 | 
			
		||||
    "estimateInBracketsShort": "(预计.)",
 | 
			
		||||
    "estimateInBracketsShort": "(预计)",
 | 
			
		||||
    "notInstalled": "未安装",
 | 
			
		||||
    "estimateInBrackets": "(预计)",
 | 
			
		||||
    "estimateInBrackets": "(预计)",
 | 
			
		||||
    "selectAll": "全选",
 | 
			
		||||
    "deselectN": "取消选择 {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} 将被从 Obtainium 中删除,但仍安装在设备上。",
 | 
			
		||||
    "removeSelectedAppsQuestion": "删除已选择的应用程序吗?",
 | 
			
		||||
    "removeSelectedApps": "删除已选择的应用程序",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
 | 
			
		||||
    "removeSelectedAppsQuestion": "是否删除选中的应用?",
 | 
			
		||||
    "removeSelectedApps": "删除选中的应用",
 | 
			
		||||
    "updateX": "更新 {}",
 | 
			
		||||
    "installX": "安装 {}",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "将仅追踪编辑为已更新",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "将 {}\n(仅追踪)\n标记为已更新",
 | 
			
		||||
    "changeX": "更改 {}",
 | 
			
		||||
    "installUpdateApps": "安装/更新应用程序",
 | 
			
		||||
    "installUpdateSelectedApps": "安装/更新已选择的应用程序",
 | 
			
		||||
    "onlyAppliesToInstalledAndOutdatedApps": "'只适用于已安装但已过时的应用程序",
 | 
			
		||||
    "markXSelectedAppsAsUpdated": "将已选择的 {} 个应用程序标记为已更新?",
 | 
			
		||||
    "installUpdateApps": "安装/更新应用",
 | 
			
		||||
    "installUpdateSelectedApps": "安装/更新选中的应用",
 | 
			
		||||
    "markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
 | 
			
		||||
    "no": "不要",
 | 
			
		||||
    "yes": "好的",
 | 
			
		||||
    "markSelectedAppsUpdated": "标记已选择的应用程序为已更新",
 | 
			
		||||
    "markSelectedAppsUpdated": "将选中的应用标记为已更新",
 | 
			
		||||
    "pinToTop": "置顶",
 | 
			
		||||
    "unpinFromTop": "取消置顶",
 | 
			
		||||
    "resetInstallStatusForSelectedAppsQuestion": "为已选择的应用程序重置安装状态吗?",
 | 
			
		||||
    "installStatusOfXWillBeResetExplanation": "当 Obtainium 中显示的应用程序版本由于更新失败或其他问题而不正确时,这将有助于重置任何选定应用程序的安装状态。",
 | 
			
		||||
    "shareSelectedAppURLs": "分享已选择的应用程序 URL",
 | 
			
		||||
    "resetInstallStatusForSelectedAppsQuestion": "是否重置选中应用的安装状态?",
 | 
			
		||||
    "installStatusOfXWillBeResetExplanation": "选中应用的安装状态将会被重置。\n\n当更新安装失败或其他问题导致 Obtainium 中的应用版本显示错误时,可以尝试通过此方法解决。",
 | 
			
		||||
    "shareSelectedAppURLs": "分享选中应用的 URL",
 | 
			
		||||
    "resetInstallStatus": "重置安装状态",
 | 
			
		||||
    "more": "更多",
 | 
			
		||||
    "removeOutdatedFilter": "删除过时的应用程序过滤器",
 | 
			
		||||
    "showOutdatedOnly": "只显示过时的应用程序",
 | 
			
		||||
    "filter": "过滤器",
 | 
			
		||||
    "filterActive": "过滤器 *",
 | 
			
		||||
    "filterApps": "过滤应用",
 | 
			
		||||
    "removeOutdatedFilter": "删除失效的应用筛选",
 | 
			
		||||
    "showOutdatedOnly": "只显示待更新应用",
 | 
			
		||||
    "filter": "筛选",
 | 
			
		||||
    "filterActive": "筛选 *",
 | 
			
		||||
    "filterApps": "筛选应用",
 | 
			
		||||
    "appName": "应用名称",
 | 
			
		||||
    "author": "作者",
 | 
			
		||||
    "upToDateApps": "已更新的应用程序",
 | 
			
		||||
    "nonInstalledApps": "未安装的应用程序",
 | 
			
		||||
    "upToDateApps": "无需更新的应用",
 | 
			
		||||
    "nonInstalledApps": "未安装的应用",
 | 
			
		||||
    "importExport": "导入/导出",
 | 
			
		||||
    "settings": "设置",
 | 
			
		||||
    "exportedTo": "导出到 {}",
 | 
			
		||||
    "exportedTo": "已导出至 {}",
 | 
			
		||||
    "obtainiumExport": "Obtainium 导出",
 | 
			
		||||
    "invalidInput": "无效输入",
 | 
			
		||||
    "importedX": "已导出到 {}",
 | 
			
		||||
    "invalidInput": "无效的输入",
 | 
			
		||||
    "importedX": "已导入 {}",
 | 
			
		||||
    "obtainiumImport": "Obtainium 导入",
 | 
			
		||||
    "importFromURLList": "从 URL 列表导入",
 | 
			
		||||
    "searchQuery": "搜索查询",
 | 
			
		||||
@@ -109,13 +108,13 @@
 | 
			
		||||
    "searchX": "搜索 {}",
 | 
			
		||||
    "noResults": "无结果",
 | 
			
		||||
    "importX": "导入 {}",
 | 
			
		||||
    "importedAppsIdDisclaimer": "导入的应用程序可能显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
 | 
			
		||||
    "importedAppsIdDisclaimer": "导入的应用可能错误地显示为“未安装”。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
 | 
			
		||||
    "importErrors": "导入错误",
 | 
			
		||||
    "importedXOfYApps": "{} 中的 {} 个应用已导入",
 | 
			
		||||
    "followingURLsHadErrors": "以下 URL 有错误:",
 | 
			
		||||
    "importedXOfYApps": "已导入 {} 中的 {} 个应用。",
 | 
			
		||||
    "followingURLsHadErrors": "下列 URL 存在错误:",
 | 
			
		||||
    "okay": "好的",
 | 
			
		||||
    "selectURL": "已选择的 URL",
 | 
			
		||||
    "selectURLs": "已选择的 URL",
 | 
			
		||||
    "selectURL": "选择 URL",
 | 
			
		||||
    "selectURLs": "选择 URL",
 | 
			
		||||
    "pick": "选择",
 | 
			
		||||
    "theme": "主题",
 | 
			
		||||
    "dark": "深色",
 | 
			
		||||
@@ -123,68 +122,68 @@
 | 
			
		||||
    "followSystem": "跟随系统",
 | 
			
		||||
    "obtainium": "Obtainium",
 | 
			
		||||
    "materialYou": "Material You",
 | 
			
		||||
    "useBlackTheme": "Use pure black dark theme",
 | 
			
		||||
    "appSortBy": "排列方式",
 | 
			
		||||
    "authorName": "作者 / 名字",
 | 
			
		||||
    "nameAuthor": "名字 / 作者",
 | 
			
		||||
    "asAdded": "添加顺序",
 | 
			
		||||
    "appSortOrder": "排列顺序",
 | 
			
		||||
    "useBlackTheme": "使用纯黑深色主题",
 | 
			
		||||
    "appSortBy": "排序依据",
 | 
			
		||||
    "authorName": "作者 / 应用名称",
 | 
			
		||||
    "nameAuthor": "应用名称 / 作者",
 | 
			
		||||
    "asAdded": "添加次序",
 | 
			
		||||
    "appSortOrder": "顺序",
 | 
			
		||||
    "ascending": "升序",
 | 
			
		||||
    "descending": "降序",
 | 
			
		||||
    "bgUpdateCheckInterval": "后台更新检查间隔",
 | 
			
		||||
    "neverManualOnly": "手动",
 | 
			
		||||
    "appearance": "外观",
 | 
			
		||||
    "showWebInAppView": "在应用来源页显示网页",
 | 
			
		||||
    "pinUpdates": "需更新的应用置顶",
 | 
			
		||||
    "updates": "检查间隔",
 | 
			
		||||
    "sourceSpecific": "Github 访问令牌",
 | 
			
		||||
    "showWebInAppView": "在应用详情页显示来源网页",
 | 
			
		||||
    "pinUpdates": "将待更新应用置顶",
 | 
			
		||||
    "updates": "更新",
 | 
			
		||||
    "sourceSpecific": "来源相关",
 | 
			
		||||
    "appSource": "源代码",
 | 
			
		||||
    "noLogs": "无日志",
 | 
			
		||||
    "appLogs": "应用日志",
 | 
			
		||||
    "appLogs": "日志",
 | 
			
		||||
    "close": "关闭",
 | 
			
		||||
    "share": "分享",
 | 
			
		||||
    "appNotFound": "未找到应用",
 | 
			
		||||
    "obtainiumExportHyphenatedLowercase": "obtainium-导出",
 | 
			
		||||
    "pickAnAPK": "选择一个安装包",
 | 
			
		||||
    "pickAnAPK": "选择一个 APK 文件",
 | 
			
		||||
    "appHasMoreThanOnePackage": "{} 有多个架构可用:",
 | 
			
		||||
    "deviceSupportsXArch": "你的设备支持 {} 架构",
 | 
			
		||||
    "deviceSupportsFollowingArchs": "你的设备支持以下架构:",
 | 
			
		||||
    "deviceSupportsXArch": "您的设备支持 {} 架构。",
 | 
			
		||||
    "deviceSupportsFollowingArchs": "您的设备支持下列架构:",
 | 
			
		||||
    "warning": "警告",
 | 
			
		||||
    "sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?",
 | 
			
		||||
    "sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?",
 | 
			
		||||
    "updatesAvailable": "更新可用",
 | 
			
		||||
    "updatesAvailableNotifDescription": "通知 Obtainium 所跟踪应用程序的更新",
 | 
			
		||||
    "noNewUpdates": "你的应用已是最新。",
 | 
			
		||||
    "xHasAnUpdate": "{} 有更新啦",
 | 
			
		||||
    "updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发出通知",
 | 
			
		||||
    "noNewUpdates": "全部应用已是最新。",
 | 
			
		||||
    "xHasAnUpdate": "{} 可以更新了。",
 | 
			
		||||
    "appsUpdated": "应用已更新",
 | 
			
		||||
    "appsUpdatedNotifDescription": "通知在后台安装应用程序的更新",
 | 
			
		||||
    "xWasUpdatedToY": "{} 已更新到 {}.",
 | 
			
		||||
    "appsUpdatedNotifDescription": "当应用在后台安装更新时发出通知",
 | 
			
		||||
    "xWasUpdatedToY": "{} 已更新至 {}。",
 | 
			
		||||
    "errorCheckingUpdates": "检查更新出错",
 | 
			
		||||
    "errorCheckingUpdatesNotifDescription": "当后台更新检查失败时显示的通知",
 | 
			
		||||
    "errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知",
 | 
			
		||||
    "appsRemoved": "应用已删除",
 | 
			
		||||
    "appsRemovedNotifDescription": "通知由于加载应用程序时出错而被删除",
 | 
			
		||||
    "xWasRemovedDueToErrorY": "{} 已因以下错误被删除: {}",
 | 
			
		||||
    "appsRemovedNotifDescription": "当应用因加载出错而被删除时发出通知",
 | 
			
		||||
    "xWasRemovedDueToErrorY": "{} 由于以下错误被删除:{}",
 | 
			
		||||
    "completeAppInstallation": "完成应用安装",
 | 
			
		||||
    "obtainiumMustBeOpenToInstallApps": "Obtainium 需要被启动以安装更新",
 | 
			
		||||
    "completeAppInstallationNotifDescription": "需要返回 Obtainium,以完成应用程序的安装。",
 | 
			
		||||
    "checkingForUpdates": "检查更新中",
 | 
			
		||||
    "checkingForUpdatesNotifDescription": "检查更新时出现的瞬时通知",
 | 
			
		||||
    "pleaseAllowInstallPerm": "请允许 Obtainium 安装应用程序",
 | 
			
		||||
    "obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用",
 | 
			
		||||
    "completeAppInstallationNotifDescription": "提示返回 Obtainium 以完成应用的安装",
 | 
			
		||||
    "checkingForUpdates": "正在检查更新",
 | 
			
		||||
    "checkingForUpdatesNotifDescription": "检查更新时短暂显示的通知",
 | 
			
		||||
    "pleaseAllowInstallPerm": "请授予 Obtainium 安装应用的权限",
 | 
			
		||||
    "trackOnly": "仅追踪",
 | 
			
		||||
    "errorWithHttpStatusCode": "错误 {}",
 | 
			
		||||
    "versionCorrectionDisabled": "禁用版本更正(插件似乎未起作用)",
 | 
			
		||||
    "versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
 | 
			
		||||
    "unknown": "未知",
 | 
			
		||||
    "none": "无",
 | 
			
		||||
    "never": "从不",
 | 
			
		||||
    "latestVersionX": "最新: {}",
 | 
			
		||||
    "installedVersionX": "已安装: {}",
 | 
			
		||||
    "lastUpdateCheckX": "最后检查: {}",
 | 
			
		||||
    "latestVersionX": "最新版本:{}",
 | 
			
		||||
    "installedVersionX": "当前版本:{}",
 | 
			
		||||
    "lastUpdateCheckX": "上次更新检查:{}",
 | 
			
		||||
    "remove": "删除",
 | 
			
		||||
    "yesMarkUpdated": "'是的,标为已更新",
 | 
			
		||||
    "fdroid": "F-Droid",
 | 
			
		||||
    "yesMarkUpdated": "是的,标记为已更新",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "应用 ID 或名称",
 | 
			
		||||
    "appWithIdOrNameNotFound": "没有发现具有此 ID 或名称的应用",
 | 
			
		||||
    "reposHaveMultipleApps": "来源可能包含多个应用",
 | 
			
		||||
    "fdroidThirdPartyRepo": "F-Droid 第三方源",
 | 
			
		||||
    "appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
 | 
			
		||||
    "reposHaveMultipleApps": "存储库中可能包含多个应用",
 | 
			
		||||
    "fdroidThirdPartyRepo": "F-Droid 第三方存储库",
 | 
			
		||||
    "steam": "Steam",
 | 
			
		||||
    "steamMobile": "Steam Mobile",
 | 
			
		||||
    "steamChat": "Steam Chat",
 | 
			
		||||
@@ -193,52 +192,57 @@
 | 
			
		||||
    "update": "更新",
 | 
			
		||||
    "markUpdated": "标记为已更新",
 | 
			
		||||
    "additionalOptions": "附加选项",
 | 
			
		||||
    "disableVersionDetection": "关闭版本检测",
 | 
			
		||||
    "noVersionDetectionExplanation": "此选项应只用于版本检测不能工作的应用程序",
 | 
			
		||||
    "downloadingX": "下载中 {}",
 | 
			
		||||
    "downloadNotifDescription": "通知用户下载进度",
 | 
			
		||||
    "noAPKFound": "未找到安装包",
 | 
			
		||||
    "noVersionDetection": "无版本检测",
 | 
			
		||||
    "categorize": "归档",
 | 
			
		||||
    "categories": "归档",
 | 
			
		||||
    "disableVersionDetection": "禁用版本检测",
 | 
			
		||||
    "noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
 | 
			
		||||
    "downloadingX": "正在下载 {}",
 | 
			
		||||
    "downloadNotifDescription": "提示应用的下载进度",
 | 
			
		||||
    "noAPKFound": "未找到 APK 文件",
 | 
			
		||||
    "noVersionDetection": "禁用版本检测",
 | 
			
		||||
    "categorize": "分类",
 | 
			
		||||
    "categories": "类别",
 | 
			
		||||
    "category": "类别",
 | 
			
		||||
    "noCategory": "无类别",
 | 
			
		||||
    "noCategories": "无类别",
 | 
			
		||||
    "deleteCategoriesQuestion": "删除所有类别?",
 | 
			
		||||
    "categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别",
 | 
			
		||||
    "deleteCategoriesQuestion": "是否删除选中的类别?",
 | 
			
		||||
    "categoryDeleteWarning": "被删除类别下的应用将恢复为未分类状态。",
 | 
			
		||||
    "addCategory": "添加类别",
 | 
			
		||||
    "label": "标签",
 | 
			
		||||
    "language": "语言",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "存储权限已被拒绝",
 | 
			
		||||
    "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
 | 
			
		||||
    "filterAPKsByRegEx": "Filter APKs by Regular Expression",
 | 
			
		||||
    "removeFromObtainium": "Remove from Obtainium",
 | 
			
		||||
    "uninstallFromDevice": "Uninstall from Device",
 | 
			
		||||
    "releaseDateAsVersion": "Use Release Date as Version",
 | 
			
		||||
    "releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
 | 
			
		||||
    "changes": "Changes",
 | 
			
		||||
    "releaseDate": "Release Date",
 | 
			
		||||
    "importFromURLsInFile": "Import from URLs in File (like OPML)",
 | 
			
		||||
    "versionDetection": "Version Detection",
 | 
			
		||||
    "standardVersionDetection": "Standard version detection",
 | 
			
		||||
    "groupByCategory": "Group by Category",
 | 
			
		||||
    "autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
 | 
			
		||||
    "copiedToClipboard": "已复制至剪贴板",
 | 
			
		||||
    "storagePermissionDenied": "已拒绝授予存储权限",
 | 
			
		||||
    "selectedCategorizeWarning": "这将覆盖选中应用当前的类别设置。",
 | 
			
		||||
    "filterAPKsByRegEx": "使用正则表达式筛选 APK 文件",
 | 
			
		||||
    "removeFromObtainium": "从 Obtainium 中删除",
 | 
			
		||||
    "uninstallFromDevice": "从设备中卸载",
 | 
			
		||||
    "onlyWorksWithNonVersionDetectApps": "仅适用于禁用版本检测的应用。",
 | 
			
		||||
    "releaseDateAsVersion": "将发行日期作为版本号",
 | 
			
		||||
    "releaseDateAsVersionExplanation": "此选项应该仅用于无法进行版本检测但能够获取发行日期的应用。",
 | 
			
		||||
    "changes": "更新日志",
 | 
			
		||||
    "releaseDate": "发行日期",
 | 
			
		||||
    "importFromURLsInFile": "从文件中的 URL 导入(如 OPML)",
 | 
			
		||||
    "versionDetection": "版本检测",
 | 
			
		||||
    "standardVersionDetection": "常规版本检测",
 | 
			
		||||
    "groupByCategory": "按类别分组显示",
 | 
			
		||||
    "autoApkFilterByArch": "如果可能,尝试按 CPU 架构筛选 APK 文件",
 | 
			
		||||
    "overrideSource": "Override Source",
 | 
			
		||||
    "dontShowAgain": "Don't show this again",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "删除应用?",
 | 
			
		||||
        "other": "删除应用?"
 | 
			
		||||
        "one": "是否删除应用?",
 | 
			
		||||
        "other": "是否删除应用?"
 | 
			
		||||
    },
 | 
			
		||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
			
		||||
        "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
 | 
			
		||||
        "other": "请求过多 (API 限制) - 在 {} 分钟后重试"
 | 
			
		||||
        "one": "API 请求过于频繁(速率限制)- 在 {} 分钟后重试",
 | 
			
		||||
        "other": "API 请求过于频繁(速率限制)- 在 {} 分钟后重试"
 | 
			
		||||
    },
 | 
			
		||||
    "bgUpdateGotErrorRetryInMinutes": {
 | 
			
		||||
        "one": "后台更新检查遇到了 {} 问题, 将在 {} 分钟后重试",
 | 
			
		||||
        "other": "后台更新检查遇到了 {} 问题, 将在 {} 分钟后重试"
 | 
			
		||||
        "one": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试",
 | 
			
		||||
        "other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试"
 | 
			
		||||
    },
 | 
			
		||||
    "bgCheckFoundUpdatesWillNotifyIfNeeded": {
 | 
			
		||||
        "one": "后台更新检查找到了 {} 个更新 - 将通知用户",
 | 
			
		||||
        "other": "后台更新检查找到了 {} 个更新 - 将通知用户"
 | 
			
		||||
        "one": "后台检查发现 {} 个应用更新 - 如有需要将发出通知",
 | 
			
		||||
        "other": "后台检查发现 {} 个应用更新 - 如有需要将发出通知"
 | 
			
		||||
    },
 | 
			
		||||
    "apps": {
 | 
			
		||||
        "one": "{} 个应用",
 | 
			
		||||
@@ -261,15 +265,15 @@
 | 
			
		||||
        "other": "{} 天"
 | 
			
		||||
    },
 | 
			
		||||
    "clearedNLogsBeforeXAfterY": {
 | 
			
		||||
        "one": "清除了 {n} 个日志 (清除前 = {before}, 清除后 = {after})",
 | 
			
		||||
        "other": "清除了 {n} 个日志 (清除前 = {before}, 清除后 = {after})"
 | 
			
		||||
        "one": "清除了 {n} 个日志({before} 之前,{after} 之后)",
 | 
			
		||||
        "other": "清除了 {n} 个日志({before} 之前,{after} 之后)"
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesAvailable": {
 | 
			
		||||
        "one": "{} 和 {} 更多应用已被更新",
 | 
			
		||||
        "other": "{} 和 {} 更多应用已被更新"
 | 
			
		||||
        "one": "{} 和另外 1 个应用可以更新了。",
 | 
			
		||||
        "other": "{} 和另外 {} 个应用可以更新了。"
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesInstalled": {
 | 
			
		||||
        "one": "{} 和 {} 更多应用已被安装",
 | 
			
		||||
        "other": "{} 和 {} 更多应用已被安装"
 | 
			
		||||
        "one": "{} 和另外 1 个应用已更新。",
 | 
			
		||||
        "other": "{} 和另外 {} 个应用已更新。"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ class APKMirror extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ class Codeberg extends AppSource {
 | 
			
		||||
  var gh = GitHub();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,16 +12,15 @@ class FDroid extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegExB =
 | 
			
		||||
        RegExp('^https?://(cloudflare\\.)?$host/+[^/]+/+packages/+[^/]+');
 | 
			
		||||
        RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match != null) {
 | 
			
		||||
      url =
 | 
			
		||||
          'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
 | 
			
		||||
    }
 | 
			
		||||
    RegExp standardUrlRegExA =
 | 
			
		||||
        RegExp('^https?://(cloudflare\\.)?$host/+packages/+[^/]+');
 | 
			
		||||
    RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
 | 
			
		||||
    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -19,17 +19,6 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegExp =
 | 
			
		||||
        RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
    }
 | 
			
		||||
    return url.substring(0, match.end);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ class GitHub extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ class GitLab extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class HTML extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -41,9 +41,14 @@ class HTML extends AppSource {
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
          // is relative
 | 
			
		||||
        }
 | 
			
		||||
        var currPathSegments = uri.path.split('/');
 | 
			
		||||
        var currPathSegments = uri.path
 | 
			
		||||
            .split('/')
 | 
			
		||||
            .where((element) => element.trim().isNotEmpty)
 | 
			
		||||
            .toList();
 | 
			
		||||
        if (e.startsWith('/') || currPathSegments.isEmpty) {
 | 
			
		||||
          return '${uri.origin}/$e';
 | 
			
		||||
        } else if (e.split('/').length == 1) {
 | 
			
		||||
          return '${uri.origin}/${currPathSegments.join('/')}/$e';
 | 
			
		||||
        } else {
 | 
			
		||||
          return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class IzzyOnDroid extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ class Mullvad extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class NeutronCode extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class Signal extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class SourceForge extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ class SteamMobile extends AppSource {
 | 
			
		||||
  final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')};
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class TelegramApp extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ class VLC extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class WhatsApp extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
 | 
			
		||||
const String currentVersion = '0.11.33';
 | 
			
		||||
const String currentVersion = '0.12.0';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/html.dart';
 | 
			
		||||
import 'package:obtainium/components/custom_app_bar.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
			
		||||
@@ -28,6 +29,7 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
 | 
			
		||||
  String userInput = '';
 | 
			
		||||
  String searchQuery = '';
 | 
			
		||||
  String? pickedSourceOverride;
 | 
			
		||||
  AppSource? pickedSource;
 | 
			
		||||
  Map<String, dynamic> additionalSettings = {};
 | 
			
		||||
  bool additionalSettingsValid = true;
 | 
			
		||||
@@ -49,8 +51,25 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
          if (isSearch) {
 | 
			
		||||
            searchnum++;
 | 
			
		||||
          }
 | 
			
		||||
          var source = valid ? sourceProvider.getSource(userInput) : null;
 | 
			
		||||
          if (pickedSource.runtimeType != source.runtimeType) {
 | 
			
		||||
          var prevHost = pickedSource?.host;
 | 
			
		||||
          try {
 | 
			
		||||
            var naturalSource =
 | 
			
		||||
                valid ? sourceProvider.getSource(userInput) : null;
 | 
			
		||||
            if (naturalSource != null &&
 | 
			
		||||
                naturalSource.runtimeType.toString() !=
 | 
			
		||||
                    HTML().runtimeType.toString()) {
 | 
			
		||||
              // If input has changed to match a regular source, reset the override
 | 
			
		||||
              pickedSourceOverride = null;
 | 
			
		||||
            }
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            // ignore
 | 
			
		||||
          }
 | 
			
		||||
          var source = valid
 | 
			
		||||
              ? sourceProvider.getSource(userInput,
 | 
			
		||||
                  overrideSource: pickedSourceOverride)
 | 
			
		||||
              : null;
 | 
			
		||||
          if (pickedSource.runtimeType != source.runtimeType ||
 | 
			
		||||
              (prevHost != null && prevHost != source?.host)) {
 | 
			
		||||
            pickedSource = source;
 | 
			
		||||
            additionalSettings = source != null
 | 
			
		||||
                ? getDefaultValuesFromFormItems(
 | 
			
		||||
@@ -64,24 +83,35 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async {
 | 
			
		||||
      return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
 | 
			
		||||
    Future<bool> getTrackOnlyConfirmationIfNeeded(
 | 
			
		||||
        bool userPickedTrackOnly, SettingsProvider settingsProvider,
 | 
			
		||||
        {bool ignoreHideSetting = false}) async {
 | 
			
		||||
      var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
 | 
			
		||||
      if (useTrackOnly &&
 | 
			
		||||
          (!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) {
 | 
			
		||||
        // ignore: use_build_context_synchronously
 | 
			
		||||
          await showDialog(
 | 
			
		||||
        var values = await showDialog(
 | 
			
		||||
            context: context,
 | 
			
		||||
            builder: (BuildContext ctx) {
 | 
			
		||||
              return GeneratedFormModal(
 | 
			
		||||
                initValid: true,
 | 
			
		||||
                title: tr('xIsTrackOnly', args: [
 | 
			
		||||
                        pickedSource!.enforceTrackOnly
 | 
			
		||||
                            ? tr('source')
 | 
			
		||||
                            : tr('app')
 | 
			
		||||
                  pickedSource!.enforceTrackOnly ? tr('source') : tr('app')
 | 
			
		||||
                ]),
 | 
			
		||||
                      items: const [],
 | 
			
		||||
                items: [
 | 
			
		||||
                  [GeneratedFormSwitch('hide', label: tr('dontShowAgain'))]
 | 
			
		||||
                ],
 | 
			
		||||
                message:
 | 
			
		||||
                    '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
 | 
			
		||||
              );
 | 
			
		||||
                  }) ==
 | 
			
		||||
              null));
 | 
			
		||||
            });
 | 
			
		||||
        if (values != null) {
 | 
			
		||||
          settingsProvider.hideTrackOnlyWarning = values['hide'] == true;
 | 
			
		||||
        }
 | 
			
		||||
        return useTrackOnly && values != null;
 | 
			
		||||
      } else {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getReleaseDateAsVersionConfirmationIfNeeded(
 | 
			
		||||
@@ -109,16 +139,15 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
        var settingsProvider = context.read<SettingsProvider>();
 | 
			
		||||
        var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
 | 
			
		||||
        App? app;
 | 
			
		||||
        if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
 | 
			
		||||
        if ((await getTrackOnlyConfirmationIfNeeded(
 | 
			
		||||
                userPickedTrackOnly, settingsProvider)) &&
 | 
			
		||||
            (await getReleaseDateAsVersionConfirmationIfNeeded(
 | 
			
		||||
                userPickedTrackOnly))) {
 | 
			
		||||
          var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
 | 
			
		||||
          app = await sourceProvider.getApp(
 | 
			
		||||
              pickedSource!, userInput, additionalSettings,
 | 
			
		||||
              trackOnlyOverride: trackOnly);
 | 
			
		||||
          if (!trackOnly) {
 | 
			
		||||
            await settingsProvider.getInstallPermission();
 | 
			
		||||
          }
 | 
			
		||||
              trackOnlyOverride: trackOnly,
 | 
			
		||||
              overrideSource: pickedSourceOverride);
 | 
			
		||||
          // Only download the APK here if you need to for the package ID
 | 
			
		||||
          if (sourceProvider.isTempId(app) &&
 | 
			
		||||
              app.additionalSettings['trackOnly'] != true) {
 | 
			
		||||
@@ -173,9 +202,9 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
                              (value) {
 | 
			
		||||
                                try {
 | 
			
		||||
                                  sourceProvider
 | 
			
		||||
                                      .getSource(value ?? '')
 | 
			
		||||
                                      .standardizeURL(
 | 
			
		||||
                                          preStandardizeUrl(value ?? ''));
 | 
			
		||||
                                      .getSource(value ?? '',
 | 
			
		||||
                                          overrideSource: pickedSourceOverride)
 | 
			
		||||
                                      .standardizeUrl(value ?? '');
 | 
			
		||||
                                } catch (e) {
 | 
			
		||||
                                  return e is String
 | 
			
		||||
                                      ? e
 | 
			
		||||
@@ -260,6 +289,48 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget getHTMLSourceOverrideDropdown() => Column(children: [
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Expanded(
 | 
			
		||||
                  child: GeneratedForm(
 | 
			
		||||
                items: [
 | 
			
		||||
                  [
 | 
			
		||||
                    GeneratedFormDropdown(
 | 
			
		||||
                        'overrideSource',
 | 
			
		||||
                        defaultValue: HTML().runtimeType.toString(),
 | 
			
		||||
                        [
 | 
			
		||||
                          ...sourceProvider.sources.map(
 | 
			
		||||
                              (s) => MapEntry(s.runtimeType.toString(), s.name))
 | 
			
		||||
                        ],
 | 
			
		||||
                        label: tr('overrideSource'))
 | 
			
		||||
                  ]
 | 
			
		||||
                ],
 | 
			
		||||
                onValueChanges: (values, valid, isBuilding) {
 | 
			
		||||
                  fn() {
 | 
			
		||||
                    pickedSourceOverride = (values['overrideSource'] == null ||
 | 
			
		||||
                            values['overrideSource'] == '')
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : values['overrideSource'];
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  if (!isBuilding) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      fn();
 | 
			
		||||
                    });
 | 
			
		||||
                  } else {
 | 
			
		||||
                    fn();
 | 
			
		||||
                  }
 | 
			
		||||
                  changeUserInput(userInput, valid, isBuilding);
 | 
			
		||||
                },
 | 
			
		||||
              ))
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          const SizedBox(
 | 
			
		||||
            height: 25,
 | 
			
		||||
          ),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
    bool shouldShowSearchBar() =>
 | 
			
		||||
        sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
 | 
			
		||||
        pickedSource == null &&
 | 
			
		||||
@@ -309,6 +380,10 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
            const SizedBox(
 | 
			
		||||
              height: 16,
 | 
			
		||||
            ),
 | 
			
		||||
            if (pickedSourceOverride != null ||
 | 
			
		||||
                pickedSource.runtimeType.toString() ==
 | 
			
		||||
                    HTML().runtimeType.toString())
 | 
			
		||||
              getHTMLSourceOverrideDropdown(),
 | 
			
		||||
            GeneratedForm(
 | 
			
		||||
                key: Key(pickedSource.runtimeType.toString()),
 | 
			
		||||
                items: pickedSource!.combinedAppSpecificSettingFormItems,
 | 
			
		||||
@@ -379,6 +454,9 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      getUrlInputRow(),
 | 
			
		||||
                      const SizedBox(
 | 
			
		||||
                        height: 16,
 | 
			
		||||
                      ),
 | 
			
		||||
                      if (shouldShowSearchBar())
 | 
			
		||||
                        const SizedBox(
 | 
			
		||||
                          height: 16,
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,10 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
 | 
			
		||||
    var sourceProvider = SourceProvider();
 | 
			
		||||
    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,
 | 
			
		||||
            overrideSource: app.app.overrideSource)
 | 
			
		||||
        : null;
 | 
			
		||||
    if (!areDownloadsRunning && prevApp == null && app != null) {
 | 
			
		||||
      prevApp = app;
 | 
			
		||||
      getUpdate(app.app.id);
 | 
			
		||||
@@ -312,7 +315,10 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                app!.app.installedVersion = null;
 | 
			
		||||
                appsProvider.saveApps([app.app]);
 | 
			
		||||
              },
 | 
			
		||||
        child: Text(tr('resetInstallStatus')));
 | 
			
		||||
        child: Text(
 | 
			
		||||
          tr('resetInstallStatus'),
 | 
			
		||||
          textAlign: TextAlign.center,
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
    getInstallOrUpdateButton() => TextButton(
 | 
			
		||||
        onPressed: (app?.app.installedVersion == null ||
 | 
			
		||||
@@ -321,9 +327,6 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
            ? () 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) {
 | 
			
		||||
@@ -410,7 +413,7 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                            tooltip: tr('more')),
 | 
			
		||||
                      const SizedBox(width: 16.0),
 | 
			
		||||
                      Expanded(
 | 
			
		||||
                          child: !isVersionDetectionStandard &&
 | 
			
		||||
                          child: (!isVersionDetectionStandard || trackOnly) &&
 | 
			
		||||
                                  app?.app.installedVersion != null &&
 | 
			
		||||
                                  app?.app.installedVersion ==
 | 
			
		||||
                                      app?.app.latestVersion
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,11 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      if (filter.sourceFilter.isNotEmpty &&
 | 
			
		||||
          sourceProvider.getSource(app.app.url).runtimeType.toString() !=
 | 
			
		||||
          sourceProvider
 | 
			
		||||
                  .getSource(app.app.url,
 | 
			
		||||
                      overrideSource: app.app.overrideSource)
 | 
			
		||||
                  .runtimeType
 | 
			
		||||
                  .toString() !=
 | 
			
		||||
              filter.sourceFilter) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
@@ -306,8 +310,9 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChangeLogFn(int appIndex) {
 | 
			
		||||
      AppSource appSource =
 | 
			
		||||
          SourceProvider().getSource(listedApps[appIndex].app.url);
 | 
			
		||||
      AppSource appSource = SourceProvider().getSource(
 | 
			
		||||
          listedApps[appIndex].app.url,
 | 
			
		||||
          overrideSource: listedApps[appIndex].app.overrideSource);
 | 
			
		||||
      String? changesUrl =
 | 
			
		||||
          appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
 | 
			
		||||
      String? changeLog = listedApps[appIndex].app.changeLog;
 | 
			
		||||
@@ -364,7 +369,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                        child: Image(
 | 
			
		||||
                          image: const AssetImage(
 | 
			
		||||
                              'assets/graphics/icon_small.png'),
 | 
			
		||||
                          color: Colors.white.withOpacity(0.1),
 | 
			
		||||
                          color: Colors.white.withOpacity(0.3),
 | 
			
		||||
                          colorBlendMode: BlendMode.modulate,
 | 
			
		||||
                          gaplessPlayback: true,
 | 
			
		||||
                        ),
 | 
			
		||||
@@ -610,7 +615,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                      items: formItems.map((e) => [e]).toList(),
 | 
			
		||||
                      initValid: true,
 | 
			
		||||
                    );
 | 
			
		||||
                  }).then((values) {
 | 
			
		||||
                  }).then((values) async {
 | 
			
		||||
                if (values != null) {
 | 
			
		||||
                  if (values.isEmpty) {
 | 
			
		||||
                    values = getDefaultValuesFromFormItems([formItems]);
 | 
			
		||||
@@ -618,12 +623,6 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                  bool shouldInstallUpdates = values['updates'] == true;
 | 
			
		||||
                  bool shouldInstallNew = values['installs'] == true;
 | 
			
		||||
                  bool shouldMarkTrackOnlies = values['trackonlies'] == true;
 | 
			
		||||
                  (() async {
 | 
			
		||||
                    if (shouldInstallNew || shouldInstallUpdates) {
 | 
			
		||||
                      await settingsProvider.getInstallPermission();
 | 
			
		||||
                    }
 | 
			
		||||
                  })()
 | 
			
		||||
                      .then((_) {
 | 
			
		||||
                  List<String> toInstall = [];
 | 
			
		||||
                  if (shouldInstallUpdates) {
 | 
			
		||||
                    toInstall.addAll(existingUpdateIdsAllOrSelected);
 | 
			
		||||
@@ -636,11 +635,11 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                  }
 | 
			
		||||
                  appsProvider
 | 
			
		||||
                      .downloadAndInstallLatestApps(
 | 
			
		||||
                            toInstall, globalNavigatorKey.currentContext)
 | 
			
		||||
                          toInstall, globalNavigatorKey.currentContext,
 | 
			
		||||
                          settingsProvider: settingsProvider)
 | 
			
		||||
                      .catchError((e) {
 | 
			
		||||
                    showError(e, context);
 | 
			
		||||
                  });
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            };
 | 
			
		||||
 
 | 
			
		||||
@@ -286,6 +286,34 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                                    })
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            height16,
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                Text(tr('dontShowTrackOnlyWarnings')),
 | 
			
		||||
                                Switch(
 | 
			
		||||
                                    value:
 | 
			
		||||
                                        settingsProvider.hideTrackOnlyWarning,
 | 
			
		||||
                                    onChanged: (value) {
 | 
			
		||||
                                      settingsProvider.hideTrackOnlyWarning =
 | 
			
		||||
                                          value;
 | 
			
		||||
                                    })
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            height16,
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                Text(tr('dontShowAPKOriginWarnings')),
 | 
			
		||||
                                Switch(
 | 
			
		||||
                                    value:
 | 
			
		||||
                                        settingsProvider.hideAPKOriginWarning,
 | 
			
		||||
                                    onChanged: (value) {
 | 
			
		||||
                                      settingsProvider.hideAPKOriginWarning =
 | 
			
		||||
                                          value;
 | 
			
		||||
                                    })
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            const Divider(
 | 
			
		||||
                              height: 16,
 | 
			
		||||
                            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -172,7 +172,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      String downloadUrl = await SourceProvider()
 | 
			
		||||
          .getSource(app.url)
 | 
			
		||||
          .getSource(app.url, overrideSource: app.overrideSource)
 | 
			
		||||
          .apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value);
 | 
			
		||||
      var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
 | 
			
		||||
      var notif = DownloadNotification(app.finalName, 100);
 | 
			
		||||
@@ -204,7 +204,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      // The former case should be handled (give the App its real ID), the latter is a security issue
 | 
			
		||||
      var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
 | 
			
		||||
      if (app.id != newInfo.packageName) {
 | 
			
		||||
        if (apps[app.id] != null && !SourceProvider().isTempId(app)) {
 | 
			
		||||
        var isTempId = SourceProvider().isTempId(app);
 | 
			
		||||
        if (apps[app.id] != null && !isTempId) {
 | 
			
		||||
          throw IDChangedError();
 | 
			
		||||
        }
 | 
			
		||||
        var originalAppId = app.id;
 | 
			
		||||
@@ -213,7 +214,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
            '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
 | 
			
		||||
        if (apps[originalAppId] != null) {
 | 
			
		||||
          await removeApps([originalAppId]);
 | 
			
		||||
          await saveApps([app]);
 | 
			
		||||
          await saveApps([app], onlyIfExists: !isTempId);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return DownloadedApk(app.id, downloadedFile);
 | 
			
		||||
@@ -331,7 +332,10 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        getHost(apkUrl.value) != getHost(app.url) &&
 | 
			
		||||
        context != null) {
 | 
			
		||||
      // ignore: use_build_context_synchronously
 | 
			
		||||
      if (await showDialog(
 | 
			
		||||
      var settingsProvider = context.read<SettingsProvider>();
 | 
			
		||||
      if (!(settingsProvider.hideAPKOriginWarning) &&
 | 
			
		||||
          // ignore: use_build_context_synchronously
 | 
			
		||||
          await showDialog(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  builder: (BuildContext ctx) {
 | 
			
		||||
                    return APKOriginWarningDialog(
 | 
			
		||||
@@ -350,7 +354,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
  // If user input is needed and the App is in the background, a notification is sent to get the user's attention
 | 
			
		||||
  // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
 | 
			
		||||
  Future<List<String>> downloadAndInstallLatestApps(
 | 
			
		||||
      List<String> appIds, BuildContext? context) async {
 | 
			
		||||
      List<String> appIds, BuildContext? context,
 | 
			
		||||
      {SettingsProvider? settingsProvider}) async {
 | 
			
		||||
    List<String> appsToInstall = [];
 | 
			
		||||
    List<String> trackOnlyAppsToUpdate = [];
 | 
			
		||||
    // For all specified Apps, filter out those for which:
 | 
			
		||||
@@ -439,6 +444,11 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    silentUpdates = moveObtainiumToStart(silentUpdates);
 | 
			
		||||
    regularInstalls = moveObtainiumToStart(regularInstalls);
 | 
			
		||||
 | 
			
		||||
    if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
 | 
			
		||||
        true)) {
 | 
			
		||||
      throw ObtainiumError(tr('cancelled'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // // Install silent updates (uncomment when it works - TODO)
 | 
			
		||||
    // for (var u in silentUpdates) {
 | 
			
		||||
    //   await installApk(u, silent: true); // Would need to add silent option
 | 
			
		||||
@@ -646,7 +656,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    for (int i = 0; i < newApps.length; i++) {
 | 
			
		||||
      var info = await getInstalledInfo(newApps[i].id);
 | 
			
		||||
      try {
 | 
			
		||||
        sp.getSource(newApps[i].url);
 | 
			
		||||
        sp.getSource(newApps[i].url, overrideSource: newApps[i].overrideSource);
 | 
			
		||||
        apps[newApps[i].id] = AppInMemory(newApps[i], null, info);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        errors.add([newApps[i].id, newApps[i].finalName, e.toString()]);
 | 
			
		||||
@@ -786,7 +796,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    App? currentApp = apps[appId]!.app;
 | 
			
		||||
    SourceProvider sourceProvider = SourceProvider();
 | 
			
		||||
    App newApp = await sourceProvider.getApp(
 | 
			
		||||
        sourceProvider.getSource(currentApp.url),
 | 
			
		||||
        sourceProvider.getSource(currentApp.url,
 | 
			
		||||
            overrideSource: currentApp.overrideSource),
 | 
			
		||||
        currentApp.url,
 | 
			
		||||
        currentApp.additionalSettings,
 | 
			
		||||
        currentApp: currentApp);
 | 
			
		||||
 
 | 
			
		||||
@@ -120,16 +120,20 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> getInstallPermission() async {
 | 
			
		||||
  Future<bool> getInstallPermission({bool enforce = false}) async {
 | 
			
		||||
    while (!(await Permission.requestInstallPackages.isGranted)) {
 | 
			
		||||
      // Explicit request as InstallPlugin request sometimes bugged
 | 
			
		||||
      Fluttertoast.showToast(
 | 
			
		||||
          msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
 | 
			
		||||
      if ((await Permission.requestInstallPackages.request()) ==
 | 
			
		||||
          PermissionStatus.granted) {
 | 
			
		||||
        break;
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      if (!enforce) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get showAppWebpage {
 | 
			
		||||
@@ -159,6 +163,24 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get hideTrackOnlyWarning {
 | 
			
		||||
    return prefs?.getBool('hideTrackOnlyWarning') ?? false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set hideTrackOnlyWarning(bool show) {
 | 
			
		||||
    prefs?.setBool('hideTrackOnlyWarning', show);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get hideAPKOriginWarning {
 | 
			
		||||
    return prefs?.getBool('hideAPKOriginWarning') ?? false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set hideAPKOriginWarning(bool show) {
 | 
			
		||||
    prefs?.setBool('hideAPKOriginWarning', show);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? getSettingString(String settingId) {
 | 
			
		||||
    return prefs?.getString(settingId);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -44,69 +44,17 @@ class APKDetails {
 | 
			
		||||
      {this.releaseDate, this.changeLog});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class App {
 | 
			
		||||
  late String id;
 | 
			
		||||
  late String url;
 | 
			
		||||
  late String author;
 | 
			
		||||
  late String name;
 | 
			
		||||
  String? installedVersion;
 | 
			
		||||
  late String latestVersion;
 | 
			
		||||
  List<MapEntry<String, String>> apkUrls = [];
 | 
			
		||||
  late int preferredApkIndex;
 | 
			
		||||
  late Map<String, dynamic> additionalSettings;
 | 
			
		||||
  late DateTime? lastUpdateCheck;
 | 
			
		||||
  bool pinned = false;
 | 
			
		||||
  List<String> categories;
 | 
			
		||||
  late DateTime? releaseDate;
 | 
			
		||||
  late String? changeLog;
 | 
			
		||||
  App(
 | 
			
		||||
      this.id,
 | 
			
		||||
      this.url,
 | 
			
		||||
      this.author,
 | 
			
		||||
      this.name,
 | 
			
		||||
      this.installedVersion,
 | 
			
		||||
      this.latestVersion,
 | 
			
		||||
      this.apkUrls,
 | 
			
		||||
      this.preferredApkIndex,
 | 
			
		||||
      this.additionalSettings,
 | 
			
		||||
      this.lastUpdateCheck,
 | 
			
		||||
      this.pinned,
 | 
			
		||||
      {this.categories = const [],
 | 
			
		||||
      this.releaseDate,
 | 
			
		||||
      this.changeLog});
 | 
			
		||||
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
 | 
			
		||||
    mapList.map((e) => [e.key, e.value]).toList();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
 | 
			
		||||
  }
 | 
			
		||||
assumed2DlistToStringMapList(List<dynamic> arr) =>
 | 
			
		||||
    arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
    var source = SourceProvider().getSource(json['url']);
 | 
			
		||||
// App JSON schema has changed multiple times over the many versions of Obtainium
 | 
			
		||||
// This function takes an App JSON and modifies it if needed to conform to the latest (current) version
 | 
			
		||||
appJSONCompatibilityModifiers(Map<String, dynamic> json) {
 | 
			
		||||
  var source = SourceProvider()
 | 
			
		||||
      .getSource(json['url'], overrideSource: json['overrideSource']);
 | 
			
		||||
  var formItems = source.combinedAppSpecificSettingFormItems
 | 
			
		||||
      .reduce((value, element) => [...value, ...element]);
 | 
			
		||||
  Map<String, dynamic> additionalSettings =
 | 
			
		||||
@@ -154,12 +102,12 @@ class App {
 | 
			
		||||
          item.ensureType(additionalSettings[item.key]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
    int preferredApkIndex = json['preferredApkIndex'] == null
 | 
			
		||||
        ? 0
 | 
			
		||||
        : json['preferredApkIndex'] as int;
 | 
			
		||||
  int preferredApkIndex =
 | 
			
		||||
      json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int;
 | 
			
		||||
  if (preferredApkIndex < 0) {
 | 
			
		||||
    preferredApkIndex = 0;
 | 
			
		||||
  }
 | 
			
		||||
  json['preferredApkIndex'] = preferredApkIndex;
 | 
			
		||||
  // apkUrls can either be old list or new named list apkUrls
 | 
			
		||||
  List<MapEntry<String, String>> apkUrls = [];
 | 
			
		||||
  if (json['apkUrls'] != null) {
 | 
			
		||||
@@ -167,16 +115,101 @@ class App {
 | 
			
		||||
    try {
 | 
			
		||||
      apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      apkUrls = assumed2DlistToStringMapList(List<dynamic>.from(apkUrlJson));
 | 
			
		||||
      apkUrls = List<dynamic>.from(apkUrlJson)
 | 
			
		||||
          .map((e) => MapEntry(e[0] as String, e[1] as String))
 | 
			
		||||
          .toList();
 | 
			
		||||
    }
 | 
			
		||||
    json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls));
 | 
			
		||||
  }
 | 
			
		||||
  // Arch based APK filter option should be disabled if it previously did not exist
 | 
			
		||||
    if (json['additionalSettings'] != null &&
 | 
			
		||||
        jsonDecode(json['additionalSettings'])['autoApkFilterByArch'] == null) {
 | 
			
		||||
  if (additionalSettings['autoApkFilterByArch'] == null) {
 | 
			
		||||
    additionalSettings['autoApkFilterByArch'] = false;
 | 
			
		||||
  }
 | 
			
		||||
  json['additionalSettings'] = jsonEncode(additionalSettings);
 | 
			
		||||
  // F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
 | 
			
		||||
  // This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid)
 | 
			
		||||
  // While not causing problems for existing apps from that source that were added in a previous version
 | 
			
		||||
  var overrideSourceWasUndefined = !json.keys.contains('overrideSource');
 | 
			
		||||
  if ((json['url'] as String).startsWith('https://cloudflare.f-droid.org')) {
 | 
			
		||||
    json['overrideSource'] = FDroid().runtimeType.toString();
 | 
			
		||||
  } else if (overrideSourceWasUndefined) {
 | 
			
		||||
    // Similar to above, but for third-party F-Droid repos
 | 
			
		||||
    RegExpMatch? match = RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)')
 | 
			
		||||
        .firstMatch(json['url'] as String);
 | 
			
		||||
    if (match != null) {
 | 
			
		||||
      json['overrideSource'] = FDroidRepo().runtimeType.toString();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return json;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class App {
 | 
			
		||||
  late String id;
 | 
			
		||||
  late String url;
 | 
			
		||||
  late String author;
 | 
			
		||||
  late String name;
 | 
			
		||||
  String? installedVersion;
 | 
			
		||||
  late String latestVersion;
 | 
			
		||||
  List<MapEntry<String, String>> apkUrls = [];
 | 
			
		||||
  late int preferredApkIndex;
 | 
			
		||||
  late Map<String, dynamic> additionalSettings;
 | 
			
		||||
  late DateTime? lastUpdateCheck;
 | 
			
		||||
  bool pinned = false;
 | 
			
		||||
  List<String> categories;
 | 
			
		||||
  late DateTime? releaseDate;
 | 
			
		||||
  late String? changeLog;
 | 
			
		||||
  late String? overrideSource;
 | 
			
		||||
  App(
 | 
			
		||||
      this.id,
 | 
			
		||||
      this.url,
 | 
			
		||||
      this.author,
 | 
			
		||||
      this.name,
 | 
			
		||||
      this.installedVersion,
 | 
			
		||||
      this.latestVersion,
 | 
			
		||||
      this.apkUrls,
 | 
			
		||||
      this.preferredApkIndex,
 | 
			
		||||
      this.additionalSettings,
 | 
			
		||||
      this.lastUpdateCheck,
 | 
			
		||||
      this.pinned,
 | 
			
		||||
      {this.categories = const [],
 | 
			
		||||
      this.releaseDate,
 | 
			
		||||
      this.changeLog,
 | 
			
		||||
      this.overrideSource});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    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,
 | 
			
		||||
      overrideSource: overrideSource);
 | 
			
		||||
 | 
			
		||||
  factory App.fromJson(Map<String, dynamic> json) {
 | 
			
		||||
    json = appJSONCompatibilityModifiers(json);
 | 
			
		||||
    return App(
 | 
			
		||||
        json['id'] as String,
 | 
			
		||||
        json['url'] as String,
 | 
			
		||||
@@ -186,9 +219,9 @@ class App {
 | 
			
		||||
            ? null
 | 
			
		||||
            : json['installedVersion'] as String,
 | 
			
		||||
        json['latestVersion'] as String,
 | 
			
		||||
        apkUrls,
 | 
			
		||||
        preferredApkIndex,
 | 
			
		||||
        additionalSettings,
 | 
			
		||||
        assumed2DlistToStringMapList(jsonDecode(json['apkUrls'])),
 | 
			
		||||
        json['preferredApkIndex'] as int,
 | 
			
		||||
        jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
 | 
			
		||||
        json['lastUpdateCheck'] == null
 | 
			
		||||
            ? null
 | 
			
		||||
            : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
 | 
			
		||||
@@ -204,7 +237,8 @@ class App {
 | 
			
		||||
            ? null
 | 
			
		||||
            : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
 | 
			
		||||
        changeLog:
 | 
			
		||||
            json['changeLog'] == null ? null : json['changeLog'] as String);
 | 
			
		||||
            json['changeLog'] == null ? null : json['changeLog'] as String,
 | 
			
		||||
        overrideSource: json['overrideSource']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() => {
 | 
			
		||||
@@ -214,14 +248,15 @@ class App {
 | 
			
		||||
        'name': name,
 | 
			
		||||
        'installedVersion': installedVersion,
 | 
			
		||||
        'latestVersion': latestVersion,
 | 
			
		||||
        'apkUrls': jsonEncode(apkUrls.map((e) => [e.key, e.value]).toList()),
 | 
			
		||||
        'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)),
 | 
			
		||||
        'preferredApkIndex': preferredApkIndex,
 | 
			
		||||
        'additionalSettings': jsonEncode(additionalSettings),
 | 
			
		||||
        'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
 | 
			
		||||
        'pinned': pinned,
 | 
			
		||||
        'categories': categories,
 | 
			
		||||
        'releaseDate': releaseDate?.microsecondsSinceEpoch,
 | 
			
		||||
        'changeLog': changeLog
 | 
			
		||||
        'changeLog': changeLog,
 | 
			
		||||
        'overrideSource': overrideSource
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -273,8 +308,9 @@ List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
 | 
			
		||||
      return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
 | 
			
		||||
    }).toList();
 | 
			
		||||
 | 
			
		||||
class AppSource {
 | 
			
		||||
abstract class AppSource {
 | 
			
		||||
  String? host;
 | 
			
		||||
  bool hostChanged = false;
 | 
			
		||||
  late String name;
 | 
			
		||||
  bool enforceTrackOnly = false;
 | 
			
		||||
  bool changeLogIfAnyIsMarkDown = true;
 | 
			
		||||
@@ -283,7 +319,15 @@ class AppSource {
 | 
			
		||||
    name = runtimeType.toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
  String standardizeUrl(String url) {
 | 
			
		||||
    url = preStandardizeUrl(url);
 | 
			
		||||
    if (!hostChanged) {
 | 
			
		||||
      url = sourceSpecificStandardizeURL(url);
 | 
			
		||||
    }
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    throw NotImplementedError();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -389,7 +433,7 @@ regExValidator(String? value) {
 | 
			
		||||
 | 
			
		||||
class SourceProvider {
 | 
			
		||||
  // Add more source classes here so they are available via the service
 | 
			
		||||
  List<AppSource> sources = [
 | 
			
		||||
  List<AppSource> get sources => [
 | 
			
		||||
        GitHub(),
 | 
			
		||||
        GitLab(),
 | 
			
		||||
        Codeberg(),
 | 
			
		||||
@@ -411,11 +455,22 @@ class SourceProvider {
 | 
			
		||||
  // Add more mass url source classes here so they are available via the service
 | 
			
		||||
  List<MassAppUrlSource> massUrlSources = [GitHubStars()];
 | 
			
		||||
 | 
			
		||||
  AppSource getSource(String url) {
 | 
			
		||||
  AppSource getSource(String url, {String? overrideSource}) {
 | 
			
		||||
    url = preStandardizeUrl(url);
 | 
			
		||||
    if (overrideSource != null) {
 | 
			
		||||
      var srcs =
 | 
			
		||||
          sources.where((e) => e.runtimeType.toString() == overrideSource);
 | 
			
		||||
      if (srcs.isEmpty) {
 | 
			
		||||
        throw UnsupportedURLError();
 | 
			
		||||
      }
 | 
			
		||||
      var res = srcs.first;
 | 
			
		||||
      res.host = Uri.parse(url).host;
 | 
			
		||||
      res.hostChanged = true;
 | 
			
		||||
      return srcs.first;
 | 
			
		||||
    }
 | 
			
		||||
    AppSource? source;
 | 
			
		||||
    for (var s in sources.where((element) => element.host != null)) {
 | 
			
		||||
      if (RegExp('://(.+\\.)?${s.host}').hasMatch(url)) {
 | 
			
		||||
      if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) {
 | 
			
		||||
        source = s;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -423,7 +478,7 @@ class SourceProvider {
 | 
			
		||||
    if (source == null) {
 | 
			
		||||
      for (var s in sources.where((element) => element.host == null)) {
 | 
			
		||||
        try {
 | 
			
		||||
          s.standardizeURL(url);
 | 
			
		||||
          s.sourceSpecificStandardizeURL(url);
 | 
			
		||||
          source = s;
 | 
			
		||||
          break;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
@@ -459,12 +514,14 @@ class SourceProvider {
 | 
			
		||||
 | 
			
		||||
  Future<App> getApp(
 | 
			
		||||
      AppSource source, String url, Map<String, dynamic> additionalSettings,
 | 
			
		||||
      {App? currentApp, bool trackOnlyOverride = false}) async {
 | 
			
		||||
      {App? currentApp,
 | 
			
		||||
      bool trackOnlyOverride = false,
 | 
			
		||||
      String? overrideSource}) async {
 | 
			
		||||
    if (trackOnlyOverride || source.enforceTrackOnly) {
 | 
			
		||||
      additionalSettings['trackOnly'] = true;
 | 
			
		||||
    }
 | 
			
		||||
    var trackOnly = additionalSettings['trackOnly'] == true;
 | 
			
		||||
    String standardUrl = source.standardizeURL(preStandardizeUrl(url));
 | 
			
		||||
    String standardUrl = source.standardizeUrl(url);
 | 
			
		||||
    APKDetails apk =
 | 
			
		||||
        await source.getLatestAPKDetails(standardUrl, additionalSettings);
 | 
			
		||||
    if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
 | 
			
		||||
@@ -514,7 +571,8 @@ class SourceProvider {
 | 
			
		||||
        currentApp?.pinned ?? false,
 | 
			
		||||
        categories: currentApp?.categories ?? const [],
 | 
			
		||||
        releaseDate: apk.releaseDate,
 | 
			
		||||
        changeLog: apk.changeLog);
 | 
			
		||||
        changeLog: apk.changeLog,
 | 
			
		||||
        overrideSource: overrideSource ?? currentApp?.overrideSource);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Returns errors in [results, errors] instead of throwing them
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -5,18 +5,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: android_alarm_manager_plus
 | 
			
		||||
      sha256: f6d0347734fa2ea716349a5a3e16ffdc1800ca64e5640112896d128c6815c178
 | 
			
		||||
      sha256: "88a8001851fdc9bd54fa4e30d0277bb900a50f3d86ff244da7f027400bf23ac0"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.2"
 | 
			
		||||
    version: "2.1.4"
 | 
			
		||||
  android_intent_plus:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: android_intent_plus
 | 
			
		||||
      sha256: "6bcdcd20461ac7a0c785f6298cdda96ad275d5bcbc1ecf28829cbe03ec6690be"
 | 
			
		||||
      sha256: "04cbc7c332a6f0bba88fed354de78813e9d24049c1800aaf10f449c7adc22603"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.1.7"
 | 
			
		||||
    version: "3.1.9"
 | 
			
		||||
  animations:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -117,10 +117,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: device_info_plus
 | 
			
		||||
      sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c"
 | 
			
		||||
      sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.2.0"
 | 
			
		||||
    version: "8.2.2"
 | 
			
		||||
  device_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -210,26 +210,26 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications
 | 
			
		||||
      sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0"
 | 
			
		||||
      sha256: "2876372952b65ca7f684e698eba22bda1cf581fa071dd30ba2f01900f507d0d1"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "13.0.0"
 | 
			
		||||
    version: "14.0.0+1"
 | 
			
		||||
  flutter_local_notifications_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications_linux
 | 
			
		||||
      sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89
 | 
			
		||||
      sha256: "909bb95de05a2e793503a2437146285a2f600cd0b3f826e26b870a334d8586d7"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.0+1"
 | 
			
		||||
    version: "4.0.0"
 | 
			
		||||
  flutter_local_notifications_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications_platform_interface
 | 
			
		||||
      sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab"
 | 
			
		||||
      sha256: "63235c42de5b6c99846969a27ad0209c401e6b77b0498939813725b5791c107c"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.0.0"
 | 
			
		||||
    version: "7.0.0"
 | 
			
		||||
  flutter_localizations:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description: flutter
 | 
			
		||||
@@ -247,10 +247,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_plugin_android_lifecycle
 | 
			
		||||
      sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf
 | 
			
		||||
      sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.9"
 | 
			
		||||
    version: "2.0.13"
 | 
			
		||||
  flutter_test:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description: flutter
 | 
			
		||||
@@ -417,10 +417,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_android
 | 
			
		||||
      sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a
 | 
			
		||||
      sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.25"
 | 
			
		||||
    version: "2.0.27"
 | 
			
		||||
  path_provider_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -449,10 +449,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_windows
 | 
			
		||||
      sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130
 | 
			
		||||
      sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.5"
 | 
			
		||||
    version: "2.1.6"
 | 
			
		||||
  permission_handler:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -537,10 +537,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: share_plus
 | 
			
		||||
      sha256: "692261968a494e47323dcc8bc66d8d52e81bc27cb4b808e4e8d7e8079d4cc01a"
 | 
			
		||||
      sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.3.2"
 | 
			
		||||
    version: "6.3.4"
 | 
			
		||||
  share_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -561,10 +561,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_android
 | 
			
		||||
      sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46"
 | 
			
		||||
      sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.2"
 | 
			
		||||
    version: "2.1.4"
 | 
			
		||||
  shared_preferences_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -622,18 +622,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqflite
 | 
			
		||||
      sha256: e7dfb6482d5d02b661d0b2399efa72b98909e5aa7b8336e1fb37e226264ade00
 | 
			
		||||
      sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.2.7"
 | 
			
		||||
    version: "2.2.8"
 | 
			
		||||
  sqflite_common:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqflite_common
 | 
			
		||||
      sha256: "220831bf0bd5333ff2445eee35ec131553b804e6b5d47a4a37ca6f5eb66e282c"
 | 
			
		||||
      sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.4"
 | 
			
		||||
    version: "2.4.5"
 | 
			
		||||
  stack_trace:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -710,10 +710,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_android
 | 
			
		||||
      sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411
 | 
			
		||||
      sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.0.27"
 | 
			
		||||
    version: "6.0.31"
 | 
			
		||||
  url_launcher_ios:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -726,10 +726,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_linux
 | 
			
		||||
      sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc"
 | 
			
		||||
      sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.4"
 | 
			
		||||
    version: "3.0.5"
 | 
			
		||||
  url_launcher_macos:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -758,10 +758,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_windows
 | 
			
		||||
      sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd
 | 
			
		||||
      sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.5"
 | 
			
		||||
    version: "3.0.6"
 | 
			
		||||
  uuid:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -790,10 +790,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_android
 | 
			
		||||
      sha256: "134ed5d36127b6f5865e86a82174886eae0b983dacd8df14b0448371debde755"
 | 
			
		||||
      sha256: d6cf18cd6c809c5a9294cd99707a21986aac4e08c87e1916ce2590315fb55d3a
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.6.0"
 | 
			
		||||
    version: "3.6.2"
 | 
			
		||||
  webview_flutter_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -822,10 +822,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: xdg_directories
 | 
			
		||||
      sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
 | 
			
		||||
      sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.2.0+3"
 | 
			
		||||
    version: "1.0.0"
 | 
			
		||||
  xml:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 0.11.33+155 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.12.0+160 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=2.18.2 <3.0.0'
 | 
			
		||||
@@ -38,7 +38,7 @@ dependencies:
 | 
			
		||||
  cupertino_icons: ^1.0.5
 | 
			
		||||
  path_provider: ^2.0.11
 | 
			
		||||
  flutter_fgbg: ^0.2.0 # Try removing reliance on this
 | 
			
		||||
  flutter_local_notifications: ^13.0.0
 | 
			
		||||
  flutter_local_notifications: ^14.0.0+1
 | 
			
		||||
  provider: ^6.0.3
 | 
			
		||||
  http: ^0.13.5
 | 
			
		||||
  webview_flutter: ^4.0.0
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user