Compare commits

..

5 Commits

Author SHA1 Message Date
Imran Remtulla
bf7b0c5702 Merge pull request #225 from ImranR98/dev
2 New Sources: Codeberg and HTML Fallback
2023-01-12 22:33:50 -05:00
Imran Remtulla
2972da4609 Upgraded packages 2023-01-12 22:28:47 -05:00
Imran Remtulla
b8567af98e Increment version 2023-01-12 22:24:52 -05:00
Imran Remtulla
ea62c68b40 Added the HTML fallback Source 2023-01-12 22:23:53 -05:00
Imran Remtulla
08a5af0449 Added Codeberg as a Source + search UI bugfix 2023-01-12 20:57:53 -05:00
8 changed files with 242 additions and 25 deletions

View File

@@ -9,6 +9,7 @@ Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to
Currently supported App sources: Currently supported App sources:
- [GitHub](https://github.com/) - [GitHub](https://github.com/)
- [GitLab](https://gitlab.com/) - [GitLab](https://gitlab.com/)
- [Codeberg](https://codeberg.org/)
- [F-Droid](https://f-droid.org/) - [F-Droid](https://f-droid.org/)
- [IzzyOnDroid](https://android.izzysoft.de/) - [IzzyOnDroid](https://android.izzysoft.de/)
- [Mullvad](https://mullvad.net/en/) - [Mullvad](https://mullvad.net/en/)
@@ -18,6 +19,8 @@ Currently supported App sources:
- Third Party F-Droid Repos - Third Party F-Droid Repos
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo` - Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
- [Steam](https://store.steampowered.com/mobile) - [Steam](https://store.steampowered.com/mobile)
- "HTML" (Fallback)
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
## Limitations ## Limitations
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected. - App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.

View File

@@ -0,0 +1,157 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class Codeberg extends AppSource {
Codeberg() {
host = 'codeberg.org';
additionalSourceSpecificSettingFormItems = [];
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormSwitch('includePrereleases',
label: tr('includePrereleases'), defaultValue: false)
],
[
GeneratedFormSwitch('fallbackToOlderReleases',
label: tr('fallbackToOlderReleases'), defaultValue: true)
],
[
GeneratedFormTextField('filterReleaseTitlesByRegEx',
label: tr('filterReleaseTitlesByRegEx'),
required: false,
additionalValidators: [
(value) {
if (value == null || value.isEmpty) {
return null;
}
try {
RegExp(value);
} catch (e) {
return tr('invalidRegEx');
}
return null;
}
])
]
];
canSearch = true;
}
@override
String standardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) =>
'$standardUrl/releases';
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
bool includePrereleases = additionalSettings['includePrereleases'];
bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'];
String? regexFilter =
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
?.isNotEmpty ==
true
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await get(Uri.parse(
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases'));
if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>;
List<String> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)
?.map((e) {
return e['name'] != null && e['browser_download_url'] != null
? MapEntry(e['name'] as String,
e['browser_download_url'] as String)
: const MapEntry('', '');
})
.where((element) => element.key.toLowerCase().endsWith('.apk'))
.map((e) => e.value)
.toList() ??
[];
dynamic targetRelease;
for (int i = 0; i < releases.length; i++) {
if (!fallbackToOlderReleases && i > 0) break;
if (!includePrereleases && releases[i]['prerelease'] == true) {
continue;
}
if (releases[i]['draft'] == true) {
// Draft releases not supported
}
var nameToFilter = releases[i]['name'] as String;
if (nameToFilter.trim().isEmpty) {
// Some leave titles empty so tag is used
nameToFilter = releases[i]['tag_name'] as String;
}
if (regexFilter != null &&
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
continue;
}
var apkUrls = getReleaseAPKUrls(releases[i]);
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
continue;
}
targetRelease = releases[i];
targetRelease['apkUrls'] = apkUrls;
break;
}
if (targetRelease == null) {
throw NoReleasesError();
}
String? version = targetRelease['tag_name'];
if (version == null) {
throw NoVersionError();
}
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
getAppNames(standardUrl));
} else {
throw getObtainiumHttpError(res);
}
}
AppNames getAppNames(String standardUrl) {
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
return AppNames(names[0], names[1]);
}
@override
Future<Map<String, String>> search(String query) async {
Response res = await get(Uri.parse(
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100'));
if (res.statusCode == 200) {
Map<String, String> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body)['data'] as List<dynamic>)) {
urlsWithDescriptions.addAll({
e['html_url'] as String: e['description'] != null
? e['description'] as String
: tr('noDescription')
});
}
return urlsWithDescriptions;
} else {
throw getObtainiumHttpError(res);
}
}
}

47
lib/app_sources/html.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class HTML extends AppSource {
@override
String standardizeURL(String url) {
return url;
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var uri = Uri.parse(standardUrl);
Response res = await get(uri);
if (res.statusCode == 200) {
List<String> links = parse(res.body)
.querySelectorAll('a')
.map((element) => element.attributes['href'] ?? '')
.where((element) => element.toLowerCase().endsWith('.apk'))
.toList();
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
if (links.isEmpty) {
throw NoReleasesError();
}
var rel = links.last;
var apkName = rel.split('/').last;
var version = apkName.substring(0, apkName.length - 4);
List<String> apkUrls = [rel]
.map((e) => e.toLowerCase().startsWith('http://') ||
e.toLowerCase().startsWith('https://')
? e
: '${uri.origin}/$e')
.toList();
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app')));
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.9.15'; const String currentVersion = '0.10.00';
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

View File

@@ -24,6 +24,7 @@ class AddAppPage extends StatefulWidget {
class _AddAppPageState extends State<AddAppPage> { class _AddAppPageState extends State<AddAppPage> {
bool gettingAppInfo = false; bool gettingAppInfo = false;
bool searching = false;
String userInput = ''; String userInput = '';
String searchQuery = ''; String searchQuery = '';
@@ -37,6 +38,8 @@ class _AddAppPageState extends State<AddAppPage> {
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
AppsProvider appsProvider = context.read<AppsProvider>(); AppsProvider appsProvider = context.read<AppsProvider>();
bool doingSomething = gettingAppInfo || searching;
changeUserInput(String input, bool valid, bool isBuilding) { changeUserInput(String input, bool valid, bool isBuilding) {
userInput = input; userInput = input;
if (!isBuilding) { if (!isBuilding) {
@@ -198,7 +201,7 @@ class _AddAppPageState extends State<AddAppPage> {
gettingAppInfo gettingAppInfo
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: ElevatedButton( : ElevatedButton(
onPressed: gettingAppInfo || onPressed: doingSomething ||
pickedSource == null || pickedSource == null ||
(pickedSource! (pickedSource!
.combinedAppSpecificSettingFormItems .combinedAppSpecificSettingFormItems
@@ -249,9 +252,12 @@ class _AddAppPageState extends State<AddAppPage> {
width: 16, width: 16,
), ),
ElevatedButton( ElevatedButton(
onPressed: searchQuery.isEmpty || gettingAppInfo onPressed: searchQuery.isEmpty || doingSomething
? null ? null
: () { : () {
setState(() {
searching = true;
});
Future.wait(sourceProvider.sources Future.wait(sourceProvider.sources
.where((e) => e.canSearch) .where((e) => e.canSearch)
.map((e) => .map((e) =>
@@ -293,6 +299,10 @@ class _AddAppPageState extends State<AddAppPage> {
} }
}).catchError((e) { }).catchError((e) {
showError(e, context); showError(e, context);
}).whenComplete(() {
setState(() {
searching = false;
});
}); });
}, },
child: Text(tr('search'))) child: Text(tr('search')))

View File

@@ -7,11 +7,13 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/codeberg.dart';
import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/fdroidrepo.dart'; import 'package:obtainium/app_sources/fdroidrepo.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/gitlab.dart'; import 'package:obtainium/app_sources/gitlab.dart';
import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/izzyondroid.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/app_sources/mullvad.dart'; import 'package:obtainium/app_sources/mullvad.dart';
import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/signal.dart';
import 'package:obtainium/app_sources/sourceforge.dart'; import 'package:obtainium/app_sources/sourceforge.dart';
@@ -19,7 +21,6 @@ import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/mass_app_sources/githubstars.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart';
import 'package:obtainium/providers/settings_provider.dart';
class AppNames { class AppNames {
late String author; late String author;
@@ -154,6 +155,10 @@ class App {
// Ensure the input is starts with HTTPS and has no WWW // Ensure the input is starts with HTTPS and has no WWW
preStandardizeUrl(String url) { preStandardizeUrl(String url) {
var firstDotIndex = url.indexOf('.');
if (!(firstDotIndex >= 0 && firstDotIndex != url.length - 1)) {
throw UnsupportedURLError();
}
if (url.toLowerCase().indexOf('http://') != 0 && if (url.toLowerCase().indexOf('http://') != 0 &&
url.toLowerCase().indexOf('https://') != 0) { url.toLowerCase().indexOf('https://') != 0) {
url = 'https://$url'; url = 'https://$url';
@@ -269,6 +274,7 @@ class SourceProvider {
List<AppSource> sources = [ List<AppSource> sources = [
GitHub(), GitHub(),
GitLab(), GitLab(),
Codeberg(),
FDroid(), FDroid(),
IzzyOnDroid(), IzzyOnDroid(),
Mullvad(), Mullvad(),
@@ -276,7 +282,8 @@ class SourceProvider {
SourceForge(), SourceForge(),
APKMirror(), APKMirror(),
FDroidRepo(), FDroidRepo(),
SteamMobile() SteamMobile(),
HTML() // This should ALWAYS be the last option as they are tried in order
]; ];
// Add more mass url source classes here so they are available via the service // Add more mass url source classes here so they are available via the service

View File

@@ -182,7 +182,7 @@ packages:
name: file_picker name: file_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.2.4" version: "5.2.5"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -356,7 +356,7 @@ packages:
name: mime name: mime
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.4"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -531,7 +531,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.15" version: "2.0.16"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
@@ -539,13 +539,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.14" version: "2.0.14"
shared_preferences_ios: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_ios name: shared_preferences_foundation
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.0"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
@@ -553,13 +553,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -599,14 +592,14 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.2" version: "2.2.3"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.4.0+2" version: "2.4.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -634,7 +627,7 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0+3" version: "3.0.1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -753,14 +746,14 @@ packages:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
win32: win32:
dependency: transitive dependency: transitive
description: description:
@@ -774,7 +767,7 @@ packages:
name: xdg_directories name: xdg_directories
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0+2" version: "0.2.0+3"
xml: xml:
dependency: transitive dependency: transitive
description: description:

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 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 0.9.15+105 # When changing this, update the tag in main() accordingly version: 0.10.00+106 # 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'