mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 21:36:42 +02:00
Compare commits
34 Commits
v0.9.13-be
...
v0.10.5-be
Author | SHA1 | Date | |
---|---|---|---|
4252c2711b | |||
52913b0450 | |||
427b0ed8d2 | |||
a85d6d4f08 | |||
05f712603c | |||
fa2a80e34c | |||
f43e5a2ff1 | |||
b72aa8273e | |||
520f186e4a | |||
e1e97672cf | |||
1494bcd013 | |||
3457a0a12f | |||
b165400a6e | |||
c47bf937f1 | |||
2e19a8c04c | |||
05d4da86ec | |||
e9d1b04d54 | |||
cff5334c25 | |||
a55346fc22 | |||
885df678e5 | |||
bf7b0c5702 | |||
2972da4609 | |||
b8567af98e | |||
ea62c68b40 | |||
08a5af0449 | |||
36f327c16e | |||
768213cb34 | |||
e888fb7120 | |||
1fb68dd674 | |||
5c4bb8f84c | |||
1c8e759494 | |||
081c2a07d2 | |||
02751fe8fa | |||
95f3362a84 |
@ -9,6 +9,7 @@ Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to
|
|||||||
Currently supported App sources:
|
Currently supported App sources:
|
||||||
- [GitHub](https://github.com/)
|
- [GitHub](https://github.com/)
|
||||||
- [GitLab](https://gitlab.com/)
|
- [GitLab](https://gitlab.com/)
|
||||||
|
- [Codeberg](https://codeberg.org/)
|
||||||
- [F-Droid](https://f-droid.org/)
|
- [F-Droid](https://f-droid.org/)
|
||||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||||
- [Mullvad](https://mullvad.net/en/)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
@ -18,6 +19,8 @@ Currently supported App sources:
|
|||||||
- Third Party F-Droid Repos
|
- Third Party F-Droid Repos
|
||||||
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- [Steam](https://store.steampowered.com/mobile)
|
||||||
|
- "HTML" (Fallback)
|
||||||
|
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
@ -211,6 +211,7 @@
|
|||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
||||||
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
||||||
|
@ -211,6 +211,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Too many requests (rate limited) - try again in {} minute",
|
"one": "Too many requests (rate limited) - try again in {} minute",
|
||||||
"other": "Too many requests (rate limited) - try again in {} minutes"
|
"other": "Too many requests (rate limited) - try again in {} minutes"
|
||||||
|
@ -207,9 +207,10 @@
|
|||||||
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
|
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
|
||||||
"addCategory": "Új kategória",
|
"addCategory": "Új kategória",
|
||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"language": "Language",
|
"language": "Nyelv",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Tárhely engedély megtagadva",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
||||||
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
||||||
|
@ -211,6 +211,7 @@
|
|||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
||||||
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"and": "と",
|
"and": "と",
|
||||||
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
||||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||||
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
||||||
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
||||||
"firstRun": "これがObtainiumの最初の実行です",
|
"firstRun": "これがObtainiumの最初の実行です",
|
||||||
@ -65,13 +65,13 @@
|
|||||||
"estimateInBrackets": "(推定)",
|
"estimateInBrackets": "(推定)",
|
||||||
"selectAll": "すべて選択",
|
"selectAll": "すべて選択",
|
||||||
"deselectN": "{}件の選択を解除",
|
"deselectN": "{}件の選択を解除",
|
||||||
"xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。",
|
"xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
|
||||||
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
|
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
|
||||||
"removeSelectedApps": "選択したアプリを削除する",
|
"removeSelectedApps": "選択したアプリを削除する",
|
||||||
"updateX": "{}をアップデートする",
|
"updateX": "{} をアップデートする",
|
||||||
"installX": "{}をインストールする",
|
"installX": "{} をインストールする",
|
||||||
"markXTrackOnlyAsUpdated": "{}\n(追跡のみ)\nをアップデート済みとしてマークする",
|
"markXTrackOnlyAsUpdated": "{}\n(追跡のみ)\nをアップデート済みとしてマークする",
|
||||||
"changeX": "{}を変更する",
|
"changeX": "{} を変更する",
|
||||||
"installUpdateApps": "アプリのインストール/アップデート",
|
"installUpdateApps": "アプリのインストール/アップデート",
|
||||||
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
||||||
"onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。",
|
"onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。",
|
||||||
@ -145,23 +145,23 @@
|
|||||||
"appNotFound": "アプリが見つかりません",
|
"appNotFound": "アプリが見つかりません",
|
||||||
"obtainiumExportHyphenatedLowercase": "obtainium-エクスポート",
|
"obtainiumExportHyphenatedLowercase": "obtainium-エクスポート",
|
||||||
"pickAnAPK": "APKを選択",
|
"pickAnAPK": "APKを選択",
|
||||||
"appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ",
|
"appHasMoreThanOnePackage": "{} は複数のパッケージが存在します: ",
|
||||||
"deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。",
|
"deviceSupportsXArch": "お使いのデバイスは {} CPUアーキテクチャに対応しています。",
|
||||||
"deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:",
|
"deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
|
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
|
||||||
"updatesAvailable": "アップデートが利用可能",
|
"updatesAvailable": "アップデートが利用可能",
|
||||||
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
||||||
"noNewUpdates": "新しいアップデートはありません",
|
"noNewUpdates": "新しいアップデートはありません",
|
||||||
"xHasAnUpdate": "{}のアップデートが利用可能です",
|
"xHasAnUpdate": "{} のアップデートが利用可能です",
|
||||||
"appsUpdated": "アプリをアップデートしました",
|
"appsUpdated": "アプリをアップデートしました",
|
||||||
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
||||||
"xWasUpdatedToY": "{}が{}にアップデートされました",
|
"xWasUpdatedToY": "{} が {} にアップデートされました",
|
||||||
"errorCheckingUpdates": "アップデート確認中のエラー",
|
"errorCheckingUpdates": "アップデート確認中のエラー",
|
||||||
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
|
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
|
||||||
"appsRemoved": "削除されたアプリ",
|
"appsRemoved": "削除されたアプリ",
|
||||||
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
|
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
|
||||||
"xWasRemovedDueToErrorY": "このエラーのため、{}は削除されました: {}",
|
"xWasRemovedDueToErrorY": "このエラーのため、{} は削除されました: {}",
|
||||||
"completeAppInstallation": "アプリのインストールを完了する",
|
"completeAppInstallation": "アプリのインストールを完了する",
|
||||||
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。",
|
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。",
|
||||||
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。",
|
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。",
|
||||||
@ -209,8 +209,9 @@
|
|||||||
"addCategory": "カテゴリを追加",
|
"addCategory": "カテゴリを追加",
|
||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
||||||
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
||||||
@ -248,11 +249,11 @@
|
|||||||
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
|
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesAvailable": {
|
"xAndNMoreUpdatesAvailable": {
|
||||||
"one": "{}とさらに{}個のアプリのアップデートが利用可能です",
|
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
|
||||||
"other": "{}とさらに{}個のアプリのアップデートが利用可能です"
|
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesInstalled": {
|
"xAndNMoreUpdatesInstalled": {
|
||||||
"one": "{}とさらに{}個のアプリがアップデートされました",
|
"one": "{} とさらに {} 個のアプリがアップデートされました",
|
||||||
"other": "{}とさらに{}個のアプリがアップデートされました"
|
"other": "{} とさらに {} 個のアプリがアップデートされました"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -211,6 +211,7 @@
|
|||||||
"language": "语言",
|
"language": "语言",
|
||||||
"storagePermissionDenied": "存储权限已被拒绝",
|
"storagePermissionDenied": "存储权限已被拒绝",
|
||||||
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||||
|
149
lib/app_sources/codeberg.dart
Normal file
149
lib/app_sources/codeberg.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class Codeberg extends AppSource {
|
||||||
|
Codeberg() {
|
||||||
|
host = 'codeberg.org';
|
||||||
|
|
||||||
|
additionalSourceSpecificSettingFormItems = [];
|
||||||
|
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('includePrereleases',
|
||||||
|
label: tr('includePrereleases'), defaultValue: false)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
||||||
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
return regExValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
canSearch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
|
'$standardUrl/releases';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
bool includePrereleases = additionalSettings['includePrereleases'];
|
||||||
|
bool fallbackToOlderReleases =
|
||||||
|
additionalSettings['fallbackToOlderReleases'];
|
||||||
|
String? regexFilter =
|
||||||
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
|
?.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
|
: null;
|
||||||
|
Response res = await get(Uri.parse(
|
||||||
|
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
|
List<String> getReleaseAPKUrls(dynamic release) =>
|
||||||
|
(release['assets'] as List<dynamic>?)
|
||||||
|
?.map((e) {
|
||||||
|
return e['name'] != null && e['browser_download_url'] != null
|
||||||
|
? MapEntry(e['name'] as String,
|
||||||
|
e['browser_download_url'] as String)
|
||||||
|
: const MapEntry('', '');
|
||||||
|
})
|
||||||
|
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
dynamic targetRelease;
|
||||||
|
|
||||||
|
for (int i = 0; i < releases.length; i++) {
|
||||||
|
if (!fallbackToOlderReleases && i > 0) break;
|
||||||
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (releases[i]['draft'] == true) {
|
||||||
|
// Draft releases not supported
|
||||||
|
}
|
||||||
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
|
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
||||||
|
// Some leave titles empty so tag is used
|
||||||
|
nameToFilter = releases[i]['tag_name'] as String;
|
||||||
|
}
|
||||||
|
if (regexFilter != null &&
|
||||||
|
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||||
|
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
targetRelease = releases[i];
|
||||||
|
targetRelease['apkUrls'] = apkUrls;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (targetRelease == null) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
String? version = targetRelease['tag_name'];
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
||||||
|
getAppNames(standardUrl));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppNames getAppNames(String standardUrl) {
|
||||||
|
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
||||||
|
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
||||||
|
return AppNames(names[0], names[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, String>> search(String query) async {
|
||||||
|
Response res = await get(Uri.parse(
|
||||||
|
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
Map<String, String> urlsWithDescriptions = {};
|
||||||
|
for (var e in (jsonDecode(res.body)['data'] as List<dynamic>)) {
|
||||||
|
urlsWithDescriptions.addAll({
|
||||||
|
e['html_url'] as String: e['description'] != null
|
||||||
|
? e['description'] as String
|
||||||
|
: tr('noDescription')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return urlsWithDescriptions;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ class GitHub extends AppSource {
|
|||||||
additionalSourceSpecificSettingFormItems = [
|
additionalSourceSpecificSettingFormItems = [
|
||||||
GeneratedFormTextField('github-creds',
|
GeneratedFormTextField('github-creds',
|
||||||
label: tr('githubPATLabel'),
|
label: tr('githubPATLabel'),
|
||||||
|
password: true,
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
@ -64,15 +65,7 @@ class GitHub extends AppSource {
|
|||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
if (value == null || value.isEmpty) {
|
return regExValidator(value);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
RegExp(value);
|
|
||||||
} catch (e) {
|
|
||||||
return tr('invalidRegEx');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
@ -118,7 +111,7 @@ class GitHub extends AppSource {
|
|||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -140,10 +133,13 @@ class GitHub extends AppSource {
|
|||||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
|
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
||||||
|
// Some leave titles empty so tag is used
|
||||||
|
nameToFilter = releases[i]['tag_name'] as String;
|
||||||
|
}
|
||||||
if (regexFilter != null &&
|
if (regexFilter != null &&
|
||||||
!RegExp(regexFilter)
|
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||||
.hasMatch((releases[i]['name'] as String).trim())) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||||
|
47
lib/app_sources/html.dart
Normal file
47
lib/app_sources/html.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class HTML extends AppSource {
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
var uri = Uri.parse(standardUrl);
|
||||||
|
Response res = await get(uri);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
List<String> links = parse(res.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((element) => element.attributes['href'] ?? '')
|
||||||
|
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
||||||
|
if (links.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
var rel = links.last;
|
||||||
|
var apkName = rel.split('/').last;
|
||||||
|
var version = apkName.substring(0, apkName.length - 4);
|
||||||
|
List<String> apkUrls = [rel]
|
||||||
|
.map((e) => e.toLowerCase().startsWith('http://') ||
|
||||||
|
e.toLowerCase().startsWith('https://')
|
||||||
|
? e
|
||||||
|
: '${uri.origin}/$e')
|
||||||
|
.toList();
|
||||||
|
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app')));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ import 'dart:math';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
|
||||||
|
|
||||||
abstract class GeneratedFormItem {
|
abstract class GeneratedFormItem {
|
||||||
late String key;
|
late String key;
|
||||||
@ -24,6 +23,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
late bool required;
|
late bool required;
|
||||||
late int max;
|
late int max;
|
||||||
late String? hint;
|
late String? hint;
|
||||||
|
late bool password;
|
||||||
|
|
||||||
GeneratedFormTextField(String key,
|
GeneratedFormTextField(String key,
|
||||||
{String label = 'Input',
|
{String label = 'Input',
|
||||||
@ -32,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
List<String? Function(String? value)> additionalValidators = const [],
|
List<String? Function(String? value)> additionalValidators = const [],
|
||||||
this.required = true,
|
this.required = true,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
this.hint})
|
this.hint,
|
||||||
|
this.password = false})
|
||||||
: super(key,
|
: super(key,
|
||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
@ -129,6 +130,21 @@ class GeneratedForm extends StatefulWidget {
|
|||||||
State<GeneratedForm> createState() => _GeneratedFormState();
|
State<GeneratedForm> createState() => _GeneratedFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generates a random light color
|
||||||
|
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
||||||
|
Color generateRandomLightColor() {
|
||||||
|
// Create a random number generator
|
||||||
|
final Random random = Random();
|
||||||
|
|
||||||
|
// Generate random hue, saturation, and value values
|
||||||
|
final double hue = random.nextDouble() * 360;
|
||||||
|
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
||||||
|
final double value = 0.9 + random.nextDouble() * 0.1;
|
||||||
|
|
||||||
|
// Create a HSV color with the random values
|
||||||
|
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
||||||
|
}
|
||||||
|
|
||||||
class _GeneratedFormState extends State<GeneratedForm> {
|
class _GeneratedFormState extends State<GeneratedForm> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
Map<String, dynamic> values = {};
|
Map<String, dynamic> values = {};
|
||||||
@ -153,21 +169,6 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a random light color
|
|
||||||
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
|
||||||
Color generateRandomLightColor() {
|
|
||||||
// Create a random number generator
|
|
||||||
final Random random = Random();
|
|
||||||
|
|
||||||
// Generate random hue, saturation, and value values
|
|
||||||
final double hue = random.nextDouble() * 360;
|
|
||||||
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
|
||||||
final double value = 0.9 + random.nextDouble() * 0.1;
|
|
||||||
|
|
||||||
// Create a HSV color with the random values
|
|
||||||
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -188,6 +189,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
if (formItem is GeneratedFormTextField) {
|
if (formItem is GeneratedFormTextField) {
|
||||||
final formFieldKey = GlobalKey<FormFieldState>();
|
final formFieldKey = GlobalKey<FormFieldState>();
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
|
obscureText: formItem.password,
|
||||||
|
autocorrect: !formItem.password,
|
||||||
|
enableSuggestions: !formItem.password,
|
||||||
key: formFieldKey,
|
key: formFieldKey,
|
||||||
initialValue: values[formItem.key],
|
initialValue: values[formItem.key],
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
@ -29,7 +29,7 @@ class NoReleasesError extends ObtainiumError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NoAPKError extends ObtainiumError {
|
class NoAPKError extends ObtainiumError {
|
||||||
NoAPKError() : super(tr('noReleaseFound'));
|
NoAPKError() : super(tr('noAPKFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoVersionError extends ObtainiumError {
|
class NoVersionError extends ObtainiumError {
|
||||||
|
@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
|||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
import 'package:easy_localization/src/localization.dart';
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.9.13';
|
const String currentVersion = '0.10.5';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ class AddAppPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _AddAppPageState extends State<AddAppPage> {
|
class _AddAppPageState extends State<AddAppPage> {
|
||||||
bool gettingAppInfo = false;
|
bool gettingAppInfo = false;
|
||||||
|
bool searching = false;
|
||||||
|
|
||||||
String userInput = '';
|
String userInput = '';
|
||||||
String searchQuery = '';
|
String searchQuery = '';
|
||||||
@ -37,6 +38,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
|
|
||||||
|
bool doingSomething = gettingAppInfo || searching;
|
||||||
|
|
||||||
changeUserInput(String input, bool valid, bool isBuilding) {
|
changeUserInput(String input, bool valid, bool isBuilding) {
|
||||||
userInput = input;
|
userInput = input;
|
||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
@ -67,6 +70,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalSettings['noVersionDetection'] == true;
|
additionalSettings['noVersionDetection'] == true;
|
||||||
var cont = true;
|
var cont = true;
|
||||||
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -85,6 +89,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
cont = false;
|
cont = false;
|
||||||
}
|
}
|
||||||
if (userPickedNoVersionDetection &&
|
if (userPickedNoVersionDetection &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -198,7 +203,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
gettingAppInfo
|
gettingAppInfo
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
onPressed: gettingAppInfo ||
|
onPressed: doingSomething ||
|
||||||
pickedSource == null ||
|
pickedSource == null ||
|
||||||
(pickedSource!
|
(pickedSource!
|
||||||
.combinedAppSpecificSettingFormItems
|
.combinedAppSpecificSettingFormItems
|
||||||
@ -249,9 +254,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
width: 16,
|
width: 16,
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: searchQuery.isEmpty || gettingAppInfo
|
onPressed: searchQuery.isEmpty || doingSomething
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
|
setState(() {
|
||||||
|
searching = true;
|
||||||
|
});
|
||||||
Future.wait(sourceProvider.sources
|
Future.wait(sourceProvider.sources
|
||||||
.where((e) => e.canSearch)
|
.where((e) => e.canSearch)
|
||||||
.map((e) =>
|
.map((e) =>
|
||||||
@ -293,6 +301,10 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
searching = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(tr('search')))
|
child: Text(tr('search')))
|
||||||
|
@ -317,7 +317,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
tooltip: tr('more')),
|
tooltip: tr('more')),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: TextButton(
|
||||||
onPressed: (app?.app.installedVersion == null ||
|
onPressed: (app?.app.installedVersion == null ||
|
||||||
app?.app.installedVersion !=
|
app?.app.installedVersion !=
|
||||||
app?.app.latestVersion) &&
|
app?.app.latestVersion) &&
|
||||||
@ -356,7 +356,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? tr('update')
|
? tr('update')
|
||||||
: tr('markUpdated')))),
|
: tr('markUpdated')))),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
ElevatedButton(
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
@ -401,7 +402,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
surfaceTintColor:
|
surfaceTintColor:
|
||||||
Theme.of(context).colorScheme.error),
|
Theme.of(context).colorScheme.error),
|
||||||
child: Text(tr('remove')),
|
child: Text(tr('remove')),
|
||||||
),
|
)),
|
||||||
])),
|
])),
|
||||||
if (app?.downloadProgress != null)
|
if (app?.downloadProgress != null)
|
||||||
Padding(
|
Padding(
|
||||||
|
1032
lib/pages/apps.dart
1032
lib/pages/apps.dart
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ import 'package:obtainium/components/generated_form.dart';
|
|||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
@ -28,6 +29,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
var appsProvider = context.read<AppsProvider>();
|
var appsProvider = context.read<AppsProvider>();
|
||||||
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
var outlineButtonStyle = ButtonStyle(
|
var outlineButtonStyle = ButtonStyle(
|
||||||
shape: MaterialStateProperty.all(
|
shape: MaterialStateProperty.all(
|
||||||
StadiumBorder(
|
StadiumBorder(
|
||||||
@ -100,6 +102,21 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
appsProvider
|
appsProvider
|
||||||
.importApps(data)
|
.importApps(data)
|
||||||
.then((value) {
|
.then((value) {
|
||||||
|
var cats =
|
||||||
|
settingsProvider.categories;
|
||||||
|
appsProvider.apps
|
||||||
|
.forEach((key, value) {
|
||||||
|
for (var c
|
||||||
|
in value.app.categories) {
|
||||||
|
if (!cats.containsKey(c)) {
|
||||||
|
cats[c] =
|
||||||
|
generateRandomLightColor()
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
settingsProvider.categories =
|
||||||
|
cats;
|
||||||
showError(
|
showError(
|
||||||
tr('importedX', args: [
|
tr('importedX', args: [
|
||||||
plural('apps', value)
|
plural('apps', value)
|
||||||
|
@ -4,10 +4,8 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
@ -247,10 +247,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
!(await canDowngradeApps())) {
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
if (appInfo == null ||
|
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
|
||||||
int.parse(newInfo.buildNumber) > appInfo.versionCode!) {
|
|
||||||
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
|
|
||||||
}
|
|
||||||
apps[file.appId]!.app.installedVersion =
|
apps[file.appId]!.app.installedVersion =
|
||||||
apps[file.appId]!.app.latestVersion;
|
apps[file.appId]!.app.latestVersion;
|
||||||
// Don't correct install status as installation may not be done yet
|
// Don't correct install status as installation may not be done yet
|
||||||
@ -265,6 +262,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
|
||||||
if (app.apkUrls.length > 1 && context != null) {
|
if (app.apkUrls.length > 1 && context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
apkUrl = await showDialog(
|
apkUrl = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -284,6 +282,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
getHost(apkUrl) != getHost(app.url) &&
|
getHost(apkUrl) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
|
@ -7,11 +7,13 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||||
|
import 'package:obtainium/app_sources/codeberg.dart';
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/app_sources/gitlab.dart';
|
import 'package:obtainium/app_sources/gitlab.dart';
|
||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
@ -19,7 +21,6 @@ import 'package:obtainium/app_sources/steammobile.dart';
|
|||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
|
||||||
|
|
||||||
class AppNames {
|
class AppNames {
|
||||||
late String author;
|
late String author;
|
||||||
@ -154,6 +155,10 @@ class App {
|
|||||||
|
|
||||||
// Ensure the input is starts with HTTPS and has no WWW
|
// Ensure the input is starts with HTTPS and has no WWW
|
||||||
preStandardizeUrl(String url) {
|
preStandardizeUrl(String url) {
|
||||||
|
var firstDotIndex = url.indexOf('.');
|
||||||
|
if (!(firstDotIndex >= 0 && firstDotIndex != url.length - 1)) {
|
||||||
|
throw UnsupportedURLError();
|
||||||
|
}
|
||||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||||
url.toLowerCase().indexOf('https://') != 0) {
|
url.toLowerCase().indexOf('https://') != 0) {
|
||||||
url = 'https://$url';
|
url = 'https://$url';
|
||||||
@ -220,7 +225,19 @@ class AppSource {
|
|||||||
label: tr('trackOnly'),
|
label: tr('trackOnly'),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))]
|
[
|
||||||
|
GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('apkFilterRegEx',
|
||||||
|
label: tr('filterAPKsByRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
return regExValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Previous 2 variables combined into one at runtime for convenient usage
|
// Previous 2 variables combined into one at runtime for convenient usage
|
||||||
@ -264,11 +281,24 @@ abstract class MassAppUrlSource {
|
|||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
regExValidator(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RegExp(value);
|
||||||
|
} catch (e) {
|
||||||
|
return tr('invalidRegEx');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
// Add more source classes here so they are available via the service
|
// Add more source classes here so they are available via the service
|
||||||
List<AppSource> sources = [
|
List<AppSource> sources = [
|
||||||
GitHub(),
|
GitHub(),
|
||||||
GitLab(),
|
GitLab(),
|
||||||
|
Codeberg(),
|
||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
@ -276,7 +306,8 @@ class SourceProvider {
|
|||||||
SourceForge(),
|
SourceForge(),
|
||||||
APKMirror(),
|
APKMirror(),
|
||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
SteamMobile()
|
SteamMobile(),
|
||||||
|
HTML() // This should ALWAYS be the last option as they are tried in order
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add more mass url source classes here so they are available via the service
|
// Add more mass url source classes here so they are available via the service
|
||||||
@ -337,10 +368,13 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<App> getApp(
|
Future<App> getApp(
|
||||||
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
AppSource source,
|
||||||
{App? currentApp,
|
String url,
|
||||||
bool trackOnlyOverride = false,
|
Map<String, dynamic> additionalSettings, {
|
||||||
noVersionDetectionOverride = false}) async {
|
App? currentApp,
|
||||||
|
bool trackOnlyOverride = false,
|
||||||
|
noVersionDetectionOverride = false,
|
||||||
|
}) async {
|
||||||
if (trackOnlyOverride || source.enforceTrackOnly) {
|
if (trackOnlyOverride || source.enforceTrackOnly) {
|
||||||
additionalSettings['trackOnly'] = true;
|
additionalSettings['trackOnly'] = true;
|
||||||
}
|
}
|
||||||
@ -351,6 +385,11 @@ class SourceProvider {
|
|||||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
||||||
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
|
apk.apkUrls =
|
||||||
|
apk.apkUrls.where((element) => reg.hasMatch(element)).toList();
|
||||||
|
}
|
||||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
|
431
pubspec.lock
431
pubspec.lock
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.9.13+103 # When changing this, update the tag in main() accordingly
|
version: 0.10.5+111 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
|
Reference in New Issue
Block a user