mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-16 06:36:44 +02:00
Added Codeberg as a Source + search UI bugfix
This commit is contained in:
@ -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/)
|
||||||
|
157
lib/app_sources/codeberg.dart
Normal file
157
lib/app_sources/codeberg.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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')))
|
||||||
|
@ -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(),
|
||||||
|
Reference in New Issue
Block a user