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