Added Codeberg as a Source + search UI bugfix

This commit is contained in:
Imran Remtulla
2023-01-12 20:57:53 -05:00
parent 36f327c16e
commit 08a5af0449
4 changed files with 172 additions and 3 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/)

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

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,6 +7,7 @@ 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';
@ -19,7 +20,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;
@ -269,6 +269,7 @@ class SourceProvider {
List<AppSource> sources = [ List<AppSource> sources = [
GitHub(), GitHub(),
GitLab(), GitLab(),
Codeberg(),
FDroid(), FDroid(),
IzzyOnDroid(), IzzyOnDroid(),
Mullvad(), Mullvad(),