mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-30 20:40:16 +02:00
Compare commits
11 Commits
v0.8.12-be
...
v0.8.14-be
Author | SHA1 | Date | |
---|---|---|---|
|
d435481f0b | ||
|
a68d49c71c | ||
|
2b6a16637e | ||
|
e46e4e5dbc | ||
|
848c8eaf5e | ||
|
ebc48169a1 | ||
|
54c37641d5 | ||
|
05ad01bf85 | ||
|
049b023e01 | ||
|
f6ca5d42e8 | ||
|
6d0cac5894 |
@@ -15,6 +15,8 @@ Currently supported App sources:
|
||||
- [Signal](https://signal.org/)
|
||||
- [SourceForge](https://sourceforge.net/)
|
||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||
- Third Party F-Droid Repos (URLs ending with `/fdroid/repo`)
|
||||
- [Steam](https://store.steampowered.com/mobile)
|
||||
|
||||
## 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.
|
||||
|
@@ -20,7 +20,7 @@
|
||||
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
|
||||
"githubPATHint": "PAT must be in this format: username:token",
|
||||
"githubPATFormat": "username:token",
|
||||
"githubPATLinkText": "'About GitHub PATs",
|
||||
"githubPATLinkText": "About GitHub PATs",
|
||||
"includePrereleases": "Include prereleases",
|
||||
"fallbackToOlderReleases": "Fallback to older releases",
|
||||
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
|
||||
@@ -179,7 +179,15 @@
|
||||
"lastUpdateCheckX": "Last Update Check: {}",
|
||||
"remove": "Remove",
|
||||
"removeAppQuestion": "Remove App?",
|
||||
"yesMarkUpdated": "'Yes, Mark as Updated",
|
||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
||||
"fdroid": "F-Droid",
|
||||
"appIdOrName": "App ID or Name",
|
||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Too many requests (rate limited) - try again in {} minute",
|
||||
"other": "Too many requests (rate limited) - try again in {} minutes"
|
||||
|
@@ -52,7 +52,7 @@
|
||||
"additionalOptsFor": "Opzioni aggiuntive per {}",
|
||||
"supportedSourcesBelow": "Fonti supportate:",
|
||||
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
||||
"searchableInBrackets": "(Ricercabile)",
|
||||
"searchableInBrackets": "(ricercabile)",
|
||||
"appsString": "App",
|
||||
"noApps": "Nessuna App",
|
||||
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
||||
@@ -179,7 +179,15 @@
|
||||
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
||||
"remove": "Rimuovi",
|
||||
"removeAppQuestion": "Rimuovere App?",
|
||||
"yesMarkUpdated": "'Sì, contrassegna come aggiornato",
|
||||
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
||||
"fdroid": "F-Droid",
|
||||
"appIdOrName": "ID o nome dell'App",
|
||||
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
|
||||
"reposHaveMultipleApps": "I repository possono contenere più App",
|
||||
"fdroidThirdPartyRepo": "Repository di terze parti di F-Droid",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
||||
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
||||
|
@@ -20,7 +20,7 @@
|
||||
"githubPATLabel": "GitHub 个人访问令牌 (提高 API 限制)",
|
||||
"githubPATHint": "个人访问令牌必须为: username:token 形式",
|
||||
"githubPATFormat": "username:token",
|
||||
"githubPATLinkText": "'关于 GitHub 个人访问令牌",
|
||||
"githubPATLinkText": "关于 GitHub 个人访问令牌",
|
||||
"includePrereleases": "包含预发布版",
|
||||
"fallbackToOlderReleases": "回落到旧版",
|
||||
"filterReleaseTitlesByRegEx": "通过正则表达式过滤发布标题",
|
||||
@@ -39,10 +39,10 @@
|
||||
"app": "应用程序",
|
||||
"appsFromSourceAreTrackOnly": "来自此来源的应用为仅追踪",
|
||||
"youPickedTrackOnly": "你已选择仅追踪选项",
|
||||
"trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.",
|
||||
"trackOnlyAppDescription": "该应用程序将被跟踪更新,但 Obtainium 无法下载或安装它",
|
||||
"cancelled": "已取消",
|
||||
"appAlreadyAdded": "此应用程序已被添加",
|
||||
"alreadyUpToDateQuestion": "App Already up to Date?",
|
||||
"alreadyUpToDateQuestion": "应用已是最新?",
|
||||
"addApp": "添加应用",
|
||||
"appSourceURL": "应用来源 URL",
|
||||
"error": "错误",
|
||||
@@ -123,21 +123,21 @@
|
||||
"followSystem": "跟随系统",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"appSortBy": "应用排列方式",
|
||||
"authorName": "作者/名字",
|
||||
"nameAuthor": "名字/作者",
|
||||
"asAdded": "以添加顺序",
|
||||
"appSortOrder": "以排列顺序",
|
||||
"appSortBy": "排列方式",
|
||||
"authorName": "作者 / 名字",
|
||||
"nameAuthor": "名字 / 作者",
|
||||
"asAdded": "添加顺序",
|
||||
"appSortOrder": "排列顺序",
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"bgUpdateCheckInterval": "后台更新检查间隔",
|
||||
"neverManualOnly": "从不 - 仅手动",
|
||||
"neverManualOnly": "手动",
|
||||
"appearance": "外观",
|
||||
"showWebInAppView": "在应用来源页显示网页",
|
||||
"pinUpdates": "将需要更新的应用固定到顶部",
|
||||
"updates": "已更新",
|
||||
"sourceSpecific": "指定源",
|
||||
"appSource": "应用源",
|
||||
"updates": "检查间隔",
|
||||
"sourceSpecific": "Github 访问令牌",
|
||||
"appSource": "源代码",
|
||||
"noLogs": "无日志",
|
||||
"appLogs": "应用日志",
|
||||
"close": "关闭",
|
||||
@@ -170,16 +170,24 @@
|
||||
"pleaseAllowInstallPerm": "请允许 Obtainium 安装应用程序",
|
||||
"trackOnly": "仅追踪",
|
||||
"errorWithHttpStatusCode": "错误 {}",
|
||||
"versionCorrectionDisabled": "Version correction disabled (plugin doesn't seem to work)",
|
||||
"unknown": "Unknown",
|
||||
"none": "None",
|
||||
"never": "Never",
|
||||
"latestVersionX": "Latest Version: {}",
|
||||
"installedVersionX": "Installed Version: {}",
|
||||
"lastUpdateCheckX": "Last Update Check: {}",
|
||||
"remove": "Remove",
|
||||
"removeAppQuestion": "Remove App?",
|
||||
"yesMarkUpdated": "'Yes, Mark as Updated",
|
||||
"versionCorrectionDisabled": "禁用版本更正(插件似乎未起作用)",
|
||||
"unknown": "未知",
|
||||
"none": "无",
|
||||
"never": "从不",
|
||||
"latestVersionX": "最新: {}",
|
||||
"installedVersionX": "已安装: {}",
|
||||
"lastUpdateCheckX": "最后检查: {}",
|
||||
"remove": "删除",
|
||||
"removeAppQuestion": "删除应用?",
|
||||
"yesMarkUpdated": "'是的,标为已更新",
|
||||
"fdroid": "F-Droid",
|
||||
"appIdOrName": "应用 ID 或名称",
|
||||
"appWithIdOrNameNotFound": "没有发现具有此 ID 或名称的应用",
|
||||
"reposHaveMultipleApps": "来源可能包含多个应用",
|
||||
"fdroidThirdPartyRepo": "F-Droid 第三方源",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||
|
@@ -14,7 +14,7 @@ class APKMirror extends AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -43,13 +43,12 @@ class APKMirror extends AppSource {
|
||||
if (version == null || version.isEmpty) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(version, []);
|
||||
return APKDetails(version, [], getAppNames(standardUrl));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
||||
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
class FDroid extends AppSource {
|
||||
FDroid() {
|
||||
host = 'f-droid.org';
|
||||
name = tr('fdroid');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -20,7 +22,7 @@ class FDroid extends AppSource {
|
||||
RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
|
||||
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -29,12 +31,13 @@ class FDroid extends AppSource {
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||
|
||||
@override
|
||||
String? tryInferringAppId(String standardUrl) {
|
||||
String? tryInferringAppId(String standardUrl,
|
||||
{List<String> additionalData = const []}) {
|
||||
return Uri.parse(standardUrl).pathSegments.last;
|
||||
}
|
||||
|
||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
Response res, String apkUrlPrefix) {
|
||||
Response res, String apkUrlPrefix, String standardUrl) {
|
||||
if (res.statusCode == 200) {
|
||||
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
|
||||
if (releases.isEmpty) {
|
||||
@@ -48,7 +51,8 @@ class FDroid extends AppSource {
|
||||
.where((element) => element['versionName'] == latestVersion)
|
||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||
.toList();
|
||||
return APKDetails(latestVersion, apkUrls);
|
||||
return APKDetails(latestVersion, apkUrls,
|
||||
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
@@ -61,11 +65,7 @@ class FDroid extends AppSource {
|
||||
String? appId = tryInferringAppId(standardUrl);
|
||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),
|
||||
'https://f-droid.org/repo/$appId');
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
||||
'https://f-droid.org/repo/$appId',
|
||||
standardUrl);
|
||||
}
|
||||
}
|
||||
|
90
lib/app_sources/fdroidrepo.dart
Normal file
90
lib/app_sources/fdroidrepo.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.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 FDroidRepo extends AppSource {
|
||||
FDroidRepo() {
|
||||
name = tr('fdroidThirdPartyRepo');
|
||||
|
||||
additionalSourceAppSpecificFormItems = [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: tr('appIdOrName'),
|
||||
hint: tr('reposHaveMultipleApps'),
|
||||
required: true,
|
||||
key: 'appIdOrName')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegExp =
|
||||
RegExp('^https?://.+/fdroid/(repo(/|\\?)|repo\$)');
|
||||
RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData,
|
||||
{bool trackOnly = false}) async {
|
||||
String? appIdOrName = findGeneratedFormValueByKey(
|
||||
additionalSourceAppSpecificFormItems
|
||||
.reduce((value, element) => [...value, ...element]),
|
||||
additionalData,
|
||||
'appIdOrName');
|
||||
if (appIdOrName == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var res = await get(Uri.parse('$standardUrl/index.xml'));
|
||||
if (res.statusCode == 200) {
|
||||
var body = parse(res.body);
|
||||
var foundApps = body.querySelectorAll('application').where((element) {
|
||||
return element.attributes['id'] == appIdOrName;
|
||||
}).toList();
|
||||
if (foundApps.isEmpty) {
|
||||
foundApps = body.querySelectorAll('application').where((element) {
|
||||
return element.querySelector('name')?.innerHtml.toLowerCase() ==
|
||||
appIdOrName.toLowerCase();
|
||||
}).toList();
|
||||
}
|
||||
if (foundApps.isEmpty) {
|
||||
foundApps = body.querySelectorAll('application').where((element) {
|
||||
return element
|
||||
.querySelector('name')
|
||||
?.innerHtml
|
||||
.toLowerCase()
|
||||
.contains(appIdOrName.toLowerCase()) ??
|
||||
false;
|
||||
}).toList();
|
||||
}
|
||||
if (foundApps.isEmpty) {
|
||||
throw ObtainiumError(tr('appWithIdOrNameNotFound'));
|
||||
}
|
||||
var authorName = body.querySelector('repo')?.attributes['name'] ?? name;
|
||||
var appName =
|
||||
foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName;
|
||||
var releases = foundApps[0].querySelectorAll('package');
|
||||
String? latestVersion = releases[0].querySelector('version')?.innerHtml;
|
||||
if (latestVersion == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
List<String> apkUrls = releases
|
||||
.where((element) =>
|
||||
element.querySelector('version')?.innerHtml == latestVersion &&
|
||||
element.querySelector('apkname') != null)
|
||||
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
|
||||
.toList();
|
||||
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
}
|
@@ -90,7 +90,7 @@ class GitHub extends AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -162,14 +162,14 @@ class GitHub extends AppSource {
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(version, targetRelease['apkUrls'] as List<String>);
|
||||
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
||||
getAppNames(standardUrl));
|
||||
} else {
|
||||
rateLimitErrorCheck(res);
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
||||
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
||||
|
@@ -14,7 +14,7 @@ class GitLab extends AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -56,15 +56,9 @@ class GitLab extends AppSource {
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(version, apkUrls);
|
||||
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
// Same as GitHub
|
||||
return GitHub().getAppNames(standardUrl);
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ class IzzyOnDroid extends AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -22,7 +22,8 @@ class IzzyOnDroid extends AppSource {
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||
|
||||
@override
|
||||
String? tryInferringAppId(String standardUrl) {
|
||||
String? tryInferringAppId(String standardUrl,
|
||||
{List<String> additionalData = const []}) {
|
||||
return FDroid().tryInferringAppId(standardUrl);
|
||||
}
|
||||
|
||||
@@ -34,11 +35,7 @@ class IzzyOnDroid extends AppSource {
|
||||
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await get(
|
||||
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
|
||||
'https://android.izzysoft.de/frepo/$appId');
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last);
|
||||
'https://android.izzysoft.de/frepo/$appId',
|
||||
standardUrl);
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ class Mullvad extends AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -38,14 +38,11 @@ class Mullvad extends AppSource {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(
|
||||
version, ['https://mullvad.net/download/app/apk/latest']);
|
||||
version,
|
||||
['https://mullvad.net/download/app/apk/latest'],
|
||||
AppNames(name, 'Mullvad-VPN'));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames('Mullvad-VPN', 'Mullvad-VPN');
|
||||
}
|
||||
}
|
||||
|
@@ -30,12 +30,9 @@ class Signal extends AppSource {
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(version, apkUrls);
|
||||
return APKDetails(version, apkUrls, AppNames(name, 'Signal'));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal');
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ class SourceForge extends AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(runtimeType.toString());
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
@@ -50,15 +50,13 @@ class SourceForge extends AppSource {
|
||||
apkUrlListAllReleases // This can be used skipped for fallback support later
|
||||
.where((element) => getVersion(element) == version)
|
||||
.toList();
|
||||
return APKDetails(version, apkUrlList);
|
||||
return APKDetails(
|
||||
version,
|
||||
apkUrlList,
|
||||
AppNames(
|
||||
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames(runtimeType.toString(),
|
||||
standardUrl.substring(standardUrl.lastIndexOf('/') + 1));
|
||||
}
|
||||
}
|
||||
|
69
lib/app_sources/steammobile.dart
Normal file
69
lib/app_sources/steammobile.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.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 SteamMobile extends AppSource {
|
||||
SteamMobile() {
|
||||
host = 'store.steampowered.com';
|
||||
name = tr('steam');
|
||||
additionalSourceAppSpecificFormItems = [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: tr('app'),
|
||||
key: 'app',
|
||||
required: true,
|
||||
opts: apks.entries.toList())
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')};
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
return 'https://$host';
|
||||
}
|
||||
|
||||
@override
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData,
|
||||
{bool trackOnly = false}) async {
|
||||
Response res = await get(Uri.parse('https://$host/mobile'));
|
||||
if (res.statusCode == 200) {
|
||||
var apkNamePrefix = findGeneratedFormValueByKey(
|
||||
additionalSourceAppSpecificFormItems
|
||||
.reduce((value, element) => [...value, ...element]),
|
||||
additionalData,
|
||||
'app');
|
||||
if (apkNamePrefix == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String apkInURLRegexPattern = '/$apkNamePrefix-[^/]+\\.apk\$';
|
||||
var links = parse(res.body)
|
||||
.querySelectorAll('a')
|
||||
.map((e) => e.attributes['href'] ?? '')
|
||||
.where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e))
|
||||
.toList();
|
||||
|
||||
if (links.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var versionMatch = RegExp(apkInURLRegexPattern).firstMatch(links[0]);
|
||||
if (versionMatch == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
var version = links[0].substring(
|
||||
versionMatch.start + apkNamePrefix.length + 2, versionMatch.end - 4);
|
||||
var apkUrls = [links[0]];
|
||||
return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!));
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@ class GeneratedFormItem {
|
||||
late String id;
|
||||
late List<Widget> belowWidgets;
|
||||
late String? hint;
|
||||
late List<String>? opts;
|
||||
late List<MapEntry<String, String>>? opts;
|
||||
|
||||
GeneratedFormItem(
|
||||
{this.label = 'Input',
|
||||
@@ -28,7 +28,11 @@ class GeneratedFormItem {
|
||||
this.belowWidgets = const [],
|
||||
this.hint,
|
||||
this.opts,
|
||||
this.key = 'default'});
|
||||
this.key = 'default'}) {
|
||||
if (type != FormItemType.string) {
|
||||
required = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratedForm extends StatefulWidget {
|
||||
@@ -82,7 +86,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
return j < widget.defaultValues.length
|
||||
? widget.defaultValues[j++]
|
||||
: e.opts != null
|
||||
? e.opts!.first
|
||||
? e.opts!.first.key
|
||||
: '';
|
||||
}).toList())
|
||||
.toList();
|
||||
@@ -126,14 +130,15 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
return Text(tr('dropdownNoOptsError'));
|
||||
}
|
||||
return DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('colour')),
|
||||
decoration: InputDecoration(labelText: e.value.label),
|
||||
value: values[row.key][e.key],
|
||||
items: e.value.opts!
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
||||
.map((e) =>
|
||||
DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
values[row.key][e.key] = value ?? e.value.opts!.first;
|
||||
values[row.key][e.key] = value ?? e.value.opts!.first.key;
|
||||
someValueChanged();
|
||||
});
|
||||
});
|
||||
|
@@ -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.8.12';
|
||||
const String currentVersion = '0.8.14';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
@@ -31,6 +31,8 @@ const supportedLocales = [Locale('en'), Locale('zh'), Locale('it')];
|
||||
const fallbackLocale = Locale('en');
|
||||
const localeDir = 'assets/translations';
|
||||
|
||||
final globalNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
Future<void> loadTranslations() async {
|
||||
// See easy_localization/issues/210
|
||||
await EasyLocalizationController.initEasyLocation();
|
||||
@@ -85,7 +87,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
if (e is RateLimitError || e is SocketException) {
|
||||
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
||||
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
|
||||
args: [e.runtimeType.toString(), remainingMinutes.toString()]));
|
||||
args: [e.toString(), remainingMinutes.toString()]));
|
||||
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
||||
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
||||
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
||||
@@ -237,6 +239,7 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
navigatorKey: globalNavigatorKey,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
||||
|
@@ -5,6 +5,7 @@ import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/pages/app.dart';
|
||||
import 'package:obtainium/pages/import_export.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
@@ -40,12 +41,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
userInput = input;
|
||||
fn() {
|
||||
var source = valid ? sourceProvider.getSource(userInput) : null;
|
||||
if (pickedSource != source) {
|
||||
if (pickedSource.runtimeType != source.runtimeType) {
|
||||
pickedSource = source;
|
||||
sourceSpecificAdditionalData =
|
||||
source != null ? source.additionalSourceAppSpecificDefaults : [];
|
||||
sourceSpecificDataIsValid = source != null
|
||||
? sourceProvider.ifSourceAppsRequireAdditionalData(source)
|
||||
? !sourceProvider.ifSourceAppsRequireAdditionalData(source)
|
||||
: true;
|
||||
}
|
||||
}
|
||||
@@ -108,7 +109,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
app.preferredApkIndex = app.apkUrls.indexOf(apkUrl);
|
||||
// ignore: use_build_context_synchronously
|
||||
var downloadedApk = await appsProvider.downloadApp(app, context);
|
||||
var downloadedApk = await appsProvider.downloadApp(
|
||||
app, globalNavigatorKey.currentContext);
|
||||
app.id = downloadedApk.appId;
|
||||
}
|
||||
if (appsProvider.apps.containsKey(app.id)) {
|
||||
@@ -306,10 +308,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
height: 64,
|
||||
),
|
||||
Text(
|
||||
tr('additionalOptsFor', args: [
|
||||
pickedSource?.runtimeType.toString() ??
|
||||
tr('source')
|
||||
]),
|
||||
tr('additionalOptsFor',
|
||||
args: [pickedSource?.name ?? tr('source')]),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary)),
|
||||
@@ -381,16 +381,20 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
),
|
||||
...sourceProvider.sources
|
||||
.map((e) => GestureDetector(
|
||||
onTap: () {
|
||||
launchUrlString('https://${e.host}',
|
||||
mode:
|
||||
LaunchMode.externalApplication);
|
||||
},
|
||||
onTap: e.host != null
|
||||
? () {
|
||||
launchUrlString(
|
||||
'https://${e.host}',
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'${e.runtimeType.toString()}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||
style: const TextStyle(
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||
style: TextStyle(
|
||||
decoration: e.host != null
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
fontStyle: FontStyle.italic),
|
||||
)))
|
||||
.toList()
|
||||
|
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@@ -250,7 +251,9 @@ class _AppPageState extends State<AppPage> {
|
||||
appsProvider
|
||||
.downloadAndInstallLatestApps(
|
||||
[app!.app.id],
|
||||
context).then((res) {
|
||||
globalNavigatorKey
|
||||
.currentContext).then(
|
||||
(res) {
|
||||
if (res.isNotEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/pages/app.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
@@ -462,8 +463,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
trackOnlyUpdateIdsAllOrSelected);
|
||||
}
|
||||
appsProvider
|
||||
.downloadAndInstallLatestApps(
|
||||
toInstall, context)
|
||||
.downloadAndInstallLatestApps(toInstall,
|
||||
globalNavigatorKey.currentContext)
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
|
@@ -232,9 +232,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
return GeneratedFormModal(
|
||||
title: tr('searchX',
|
||||
args: [
|
||||
source
|
||||
.runtimeType
|
||||
.toString()
|
||||
source.name
|
||||
]),
|
||||
items: [
|
||||
[
|
||||
@@ -319,9 +317,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Text(tr('searchX', args: [
|
||||
source.runtimeType.toString()
|
||||
])))
|
||||
child: Text(
|
||||
tr('searchX', args: [source.name])))
|
||||
]))
|
||||
.toList(),
|
||||
...sourceProvider.massUrlSources
|
||||
|
@@ -274,9 +274,14 @@ class AppsProvider with ChangeNotifier {
|
||||
);
|
||||
});
|
||||
}
|
||||
getHost(String url) {
|
||||
var temp = Uri.parse(url).host.split('.');
|
||||
return temp.sublist(temp.length - 2).join('.');
|
||||
}
|
||||
|
||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||
if (apkUrl != null &&
|
||||
Uri.parse(apkUrl).origin != Uri.parse(app.url).origin &&
|
||||
getHost(apkUrl) != getHost(app.url) &&
|
||||
context != null) {
|
||||
if (await showDialog(
|
||||
context: context,
|
||||
|
@@ -8,12 +8,14 @@ import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||
import 'package:obtainium/app_sources/fdroid.dart';
|
||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/app_sources/gitlab.dart';
|
||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||
import 'package:obtainium/app_sources/mullvad.dart';
|
||||
import 'package:obtainium/app_sources/signal.dart';
|
||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||
import 'package:obtainium/app_sources/steammobile.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||
@@ -28,8 +30,9 @@ class AppNames {
|
||||
class APKDetails {
|
||||
late String version;
|
||||
late List<String> apkUrls;
|
||||
late AppNames names;
|
||||
|
||||
APKDetails(this.version, this.apkUrls);
|
||||
APKDetails(this.version, this.apkUrls, this.names);
|
||||
}
|
||||
|
||||
class App {
|
||||
@@ -106,11 +109,11 @@ class App {
|
||||
|
||||
// Ensure the input is starts with HTTPS and has no WWW
|
||||
preStandardizeUrl(String url) {
|
||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||
url.toLowerCase().indexOf('https://') != 0) {
|
||||
url = url.toLowerCase();
|
||||
if (url.indexOf('http://') != 0 && url.indexOf('https://') != 0) {
|
||||
url = 'https://$url';
|
||||
}
|
||||
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
||||
if (url.indexOf('https://www.') == 0) {
|
||||
url = 'https://${url.substring(12)}';
|
||||
}
|
||||
url = url
|
||||
@@ -135,8 +138,14 @@ List<String> getLinksFromParsedHTML(
|
||||
.toList();
|
||||
|
||||
class AppSource {
|
||||
late String host;
|
||||
String? host;
|
||||
late String name;
|
||||
bool enforceTrackOnly = false;
|
||||
|
||||
AppSource() {
|
||||
name = runtimeType.toString();
|
||||
}
|
||||
|
||||
String standardizeURL(String url) {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
@@ -147,10 +156,6 @@ class AppSource {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
// Different Sources may need different kinds of additional data for Apps
|
||||
List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = [];
|
||||
List<String> additionalSourceAppSpecificDefaults = [];
|
||||
@@ -168,7 +173,7 @@ class AppSource {
|
||||
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
|
||||
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) {
|
||||
throw NotImplementedError();
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
||||
@@ -180,7 +185,8 @@ class AppSource {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
String? tryInferringAppId(String standardUrl) {
|
||||
String? tryInferringAppId(String standardUrl,
|
||||
{List<String> additionalData = const []}) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -206,7 +212,9 @@ class SourceProvider {
|
||||
Mullvad(),
|
||||
Signal(),
|
||||
SourceForge(),
|
||||
APKMirror()
|
||||
APKMirror(),
|
||||
FDroidRepo(),
|
||||
SteamMobile()
|
||||
];
|
||||
|
||||
// Add more mass url source classes here so they are available via the service
|
||||
@@ -215,12 +223,23 @@ class SourceProvider {
|
||||
AppSource getSource(String url) {
|
||||
url = preStandardizeUrl(url);
|
||||
AppSource? source;
|
||||
for (var s in sources) {
|
||||
if (url.toLowerCase().contains('://${s.host}')) {
|
||||
for (var s in sources.where((element) => element.host != null)) {
|
||||
if (url.contains('://${s.host}')) {
|
||||
source = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (source == null) {
|
||||
for (var s in sources.where((element) => element.host == null)) {
|
||||
try {
|
||||
s.standardizeURL(url);
|
||||
source = s;
|
||||
break;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source == null) {
|
||||
throw UnsupportedURLError();
|
||||
}
|
||||
@@ -230,7 +249,7 @@ class SourceProvider {
|
||||
bool ifSourceAppsRequireAdditionalData(AppSource source) {
|
||||
for (var row in source.additionalSourceAppSpecificFormItems) {
|
||||
for (var element in row) {
|
||||
if (element.required) {
|
||||
if (element.required && element.opts == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -252,7 +271,7 @@ class SourceProvider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return sources.map((e) => e.host).contains(parts.last);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
||||
@@ -262,7 +281,6 @@ class SourceProvider {
|
||||
bool trackOnly = false,
|
||||
String? installedVersion}) async {
|
||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||
AppNames names = source.getAppNames(standardUrl);
|
||||
APKDetails apk = await source
|
||||
.getLatestAPKDetails(standardUrl, additionalData, trackOnly: trackOnly);
|
||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||
@@ -271,13 +289,14 @@ class SourceProvider {
|
||||
String apkVersion = apk.version.replaceAll('/', '-');
|
||||
return App(
|
||||
id ??
|
||||
source.tryInferringAppId(standardUrl) ??
|
||||
generateTempID(names, source),
|
||||
source.tryInferringAppId(standardUrl,
|
||||
additionalData: additionalData) ??
|
||||
generateTempID(apk.names, source),
|
||||
standardUrl,
|
||||
names.author[0].toUpperCase() + names.author.substring(1),
|
||||
apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
|
||||
name.trim().isNotEmpty
|
||||
? name
|
||||
: names.name[0].toUpperCase() + names.name.substring(1),
|
||||
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1),
|
||||
installedVersion,
|
||||
apkVersion,
|
||||
apk.apkUrls,
|
||||
|
@@ -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.8.12+76 # When changing this, update the tag in main() accordingly
|
||||
version: 0.8.14+78 # When changing this, update the tag in main() accordingly
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
|
Reference in New Issue
Block a user