Compare commits

...

11 Commits

Author SHA1 Message Date
Imran Remtulla
d435481f0b Increment version 2022-12-16 19:37:22 -05:00
Imran Remtulla
a68d49c71c Added Steam as a Source (#159) + Bugfixes 2022-12-16 19:26:07 -05:00
Imran Remtulla
2b6a16637e Merge branch 'main' of github.com:ImranR98/Obtainium 2022-12-16 18:56:06 -05:00
Imran Remtulla
e46e4e5dbc Merge pull request #157 from atilluF/Italian-TL
Update it.json
2022-12-16 18:54:18 -05:00
Imran Remtulla
848c8eaf5e Merge pull request #156 from RanTranslations/main
assets: Update Simplified Chinese translations
2022-12-16 18:54:07 -05:00
Imran Remtulla
ebc48169a1 Bugfix #158 2022-12-16 18:25:51 -05:00
atilluF
54c37641d5 Update it.json 2022-12-16 08:33:08 +01:00
JohnsonRan
05ad01bf85 assets: Update Simplified Chinese translations 2022-12-16 13:02:40 +08:00
Imran Remtulla
049b023e01 Adding from custom fdroid repos is easier (name based) 2022-12-15 21:39:05 -05:00
Imran Remtulla
f6ca5d42e8 Initial third party F-Droid repo support
Plus various bugfixes
And version increment
2022-12-15 21:22:03 -05:00
Imran Remtulla
6d0cac5894 Bugfix for switching pages while downloading #150 2022-12-15 18:57:06 -05:00
23 changed files with 337 additions and 133 deletions

View File

@@ -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.

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 限制) - 在 {} 分钟后重试"

View File

@@ -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('/');

View File

@@ -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);
}
}

View 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();
}
}
}

View File

@@ -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('/');

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}

View File

@@ -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));
}
}

View 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();
}
}
}

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -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()

View File

@@ -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();
}

View File

@@ -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);
});

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 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'