Compare commits

..

11 Commits

Author SHA1 Message Date
Imran Remtulla
602f0c3bb2 Increment version 2022-11-19 16:26:44 -05:00
Imran Remtulla
00721e8ac4 Added days filter to logs dialog (#117) 2022-11-19 16:20:42 -05:00
Imran Remtulla
d19f9101d6 Added dropdown support to generated form 2022-11-19 15:42:20 -05:00
Imran Remtulla
a4bc278e4c Increment version 2022-11-17 19:02:44 -05:00
Imran Remtulla
b04986622b IzzyOnDroid now uses API (+ other minor tweaks) 2022-11-17 19:00:27 -05:00
Imran Remtulla
2059e4fd44 Switched to F-Droid API 2022-11-17 18:36:05 -05:00
Imran Remtulla
618a1523cf Add app only downloads APK if needed 2022-11-16 20:57:58 -05:00
Imran Remtulla
ba1cdc2c73 Increment version 2022-11-14 21:10:30 -05:00
Imran Remtulla
aa2a25fffe Better GitHub error messages (#112) 2022-11-14 20:56:04 -05:00
Imran Remtulla
c8ec67aef3 Existing APKs now reused again
With partial APKs avoided (#113)
2022-11-14 20:38:02 -05:00
Imran Remtulla
9576a99a4e More efficient loading (addresses #110) 2022-11-14 12:56:52 -05:00
16 changed files with 232 additions and 180 deletions

View File

@@ -1,4 +1,5 @@
import 'package:html/parser.dart'; import 'dart:convert';
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';
@@ -28,41 +29,24 @@ class FDroid extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => null; String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; String? tryInferringAppId(String standardUrl) {
return Uri.parse(standardUrl).pathSegments.last;
}
@override APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Future<APKDetails> getLatestAPKDetails( Response res, String apkUrlPrefix) {
String standardUrl, List<String> additionalData) async {
Response res = await get(Uri.parse(standardUrl));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var releases = parse(res.body).querySelectorAll('.package-version'); List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
if (releases.isEmpty) { if (releases.isEmpty) {
throw NoReleasesError(); throw NoReleasesError();
} }
String? latestVersion = releases[0] String? latestVersion = releases[0]['versionName'];
.querySelector('.package-version-header b')
?.innerHtml
.split(' ')
.sublist(1)
.join(' ');
if (latestVersion == null) { if (latestVersion == null) {
throw NoVersionError(); throw NoVersionError();
} }
List<String> apkUrls = releases List<String> apkUrls = releases
.where((element) => .where((element) => element['versionName'] == latestVersion)
element .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.querySelector('.package-version-header b')
?.innerHtml
.split(' ')
.sublist(1)
.join(' ') ==
latestVersion)
.map((e) =>
e
.querySelector('.package-version-download a')
?.attributes['href'] ??
'')
.where((element) => element.isNotEmpty)
.toList(); .toList();
if (apkUrls.isEmpty) { if (apkUrls.isEmpty) {
throw NoAPKError(); throw NoAPKError();
@@ -73,6 +57,15 @@ class FDroid extends AppSource {
} }
} }
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async {
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 @override
AppNames getAppNames(String standardUrl) { AppNames getAppNames(String standardUrl) {
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last); return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);

View File

@@ -105,9 +105,6 @@ class GitHub extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => String? changeLogPageFromStandardUrl(String standardUrl) =>
'$standardUrl/releases'; '$standardUrl/releases';
@override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async { String standardUrl, List<String> additionalData) async {
@@ -167,14 +164,8 @@ class GitHub extends AppSource {
} }
return APKDetails(version, targetRelease['apkUrls']); return APKDetails(version, targetRelease['apkUrls']);
} else { } else {
if (res.headers['x-ratelimit-remaining'] == '0') { rateLimitErrorCheck(res);
throw RateLimitError( throw getObtainiumHttpError(res);
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
60000000)
.round());
}
throw NoReleasesError();
} }
} }
@@ -200,15 +191,17 @@ class GitHub extends AppSource {
} }
return urlsWithDescriptions; return urlsWithDescriptions;
} else { } else {
if (res.headers['x-ratelimit-remaining'] == '0') { rateLimitErrorCheck(res);
throw RateLimitError( throw getObtainiumHttpError(res);
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / }
60000000) }
.round());
} rateLimitErrorCheck(Response res) {
throw ObtainiumError( if (res.headers['x-ratelimit-remaining'] == '0') {
res.reasonPhrase ?? 'Error ${res.statusCode.toString()}', throw RateLimitError(
unexpected: true); (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
60000000)
.round());
} }
} }
} }

View File

@@ -23,9 +23,6 @@ class GitLab extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => String? changeLogPageFromStandardUrl(String standardUrl) =>
'$standardUrl/-/releases'; '$standardUrl/-/releases';
@override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async { String standardUrl, List<String> additionalData) async {

View File

@@ -1,5 +1,5 @@
import 'package:html/parser.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/app_sources/fdroid.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';
@@ -22,41 +22,18 @@ class IzzyOnDroid extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => null; String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; String? tryInferringAppId(String standardUrl) {
return FDroid().tryInferringAppId(standardUrl);
}
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async { String standardUrl, List<String> additionalData) async {
Response res = await get(Uri.parse(standardUrl)); String? appId = tryInferringAppId(standardUrl);
if (res.statusCode == 200) { return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
var parsedHtml = parse(res.body); await get(
var multipleVersionApkUrls = parsedHtml Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
.querySelectorAll('a') 'https://android.izzysoft.de/frepo/$appId');
.where((element) =>
element.attributes['href']?.toLowerCase().endsWith('.apk') ??
false)
.map((e) => 'https://$host${e.attributes['href'] ?? ''}')
.toList();
if (multipleVersionApkUrls.isEmpty) {
throw NoAPKError();
}
var version = parsedHtml
.querySelector('#keydata')
?.querySelectorAll('b')
.where(
(element) => element.innerHtml.toLowerCase().contains('version'))
.toList()[0]
.parentNode
?.parentNode
?.children[1]
.innerHtml;
if (version == null) {
throw NoVersionError();
}
return APKDetails(version, [multipleVersionApkUrls[0]]);
} else {
throw NoReleasesError();
}
} }
@override @override

View File

@@ -22,9 +22,6 @@ class Mullvad extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => String? changeLogPageFromStandardUrl(String standardUrl) =>
'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md'; 'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md';
@override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async { String standardUrl, List<String> additionalData) async {

View File

@@ -16,9 +16,6 @@ class Signal extends AppSource {
@override @override
String? changeLogPageFromStandardUrl(String standardUrl) => null; String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async { String standardUrl, List<String> additionalData) async {

View File

@@ -21,9 +21,6 @@ class SourceForge extends AppSource {
@override @override
String? changeLogPageFromStandardUrl(String standardUrl) => null; String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData) async { String standardUrl, List<String> additionalData) async {

View File

@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
enum FormItemType { string, bool } enum FormItemType { string, bool }
typedef OnValueChanges = void Function(List<String> values, bool valid); typedef OnValueChanges = void Function(
List<String> values, bool valid, bool isBuilding);
class GeneratedFormItem { class GeneratedFormItem {
late String label; late String label;
@@ -13,6 +14,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;
GeneratedFormItem( GeneratedFormItem(
{this.label = 'Input', {this.label = 'Input',
@@ -22,7 +24,8 @@ class GeneratedFormItem {
this.additionalValidators = const [], this.additionalValidators = const [],
this.id = 'input', this.id = 'input',
this.belowWidgets = const [], this.belowWidgets = const [],
this.hint}); this.hint,
this.opts});
} }
class GeneratedForm extends StatefulWidget { class GeneratedForm extends StatefulWidget {
@@ -47,7 +50,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
List<List<Widget>> rows = []; List<List<Widget>> rows = [];
// If any value changes, call this to update the parent with value and validity // If any value changes, call this to update the parent with value and validity
void someValueChanged() { void someValueChanged({bool isBuilding = false}) {
List<String> returnValues = []; List<String> returnValues = [];
var valid = true; var valid = true;
for (int r = 0; r < values.length; r++) { for (int r = 0; r < values.length; r++) {
@@ -62,7 +65,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
} }
} }
} }
widget.onValueChanges(returnValues, valid); widget.onValueChanges(returnValues, valid, isBuilding);
} }
@override @override
@@ -75,14 +78,16 @@ class _GeneratedFormState extends State<GeneratedForm> {
.map((row) => row.map((e) { .map((row) => row.map((e) {
return j < widget.defaultValues.length return j < widget.defaultValues.length
? widget.defaultValues[j++] ? widget.defaultValues[j++]
: ''; : e.opts != null
? e.opts!.first
: '';
}).toList()) }).toList())
.toList(); .toList();
// Dynamically create form inputs // Dynamically create form inputs
formInputs = widget.items.asMap().entries.map((row) { formInputs = widget.items.asMap().entries.map((row) {
return row.value.asMap().entries.map((e) { return row.value.asMap().entries.map((e) {
if (e.value.type == FormItemType.string) { if (e.value.type == FormItemType.string && e.value.opts == null) {
final formFieldKey = GlobalKey<FormFieldState>(); final formFieldKey = GlobalKey<FormFieldState>();
return TextFormField( return TextFormField(
key: formFieldKey, key: formFieldKey,
@@ -112,11 +117,29 @@ class _GeneratedFormState extends State<GeneratedForm> {
return null; return null;
}, },
); );
} else if (e.value.type == FormItemType.string &&
e.value.opts != null) {
if (e.value.opts!.isEmpty) {
return const Text('ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT.');
}
return DropdownButtonFormField(
decoration: const InputDecoration(labelText: 'Colour'),
value: values[row.key][e.key],
items: e.value.opts!
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (value) {
setState(() {
values[row.key][e.key] = value ?? e.value.opts!.first;
someValueChanged();
});
});
} else { } else {
return Container(); // Some input types added in build return Container(); // Some input types added in build
} }
}).toList(); }).toList();
}).toList(); }).toList();
someValueChanged(isBuilding: true);
} }
@override @override

View File

@@ -46,11 +46,16 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
), ),
GeneratedForm( GeneratedForm(
items: widget.items, items: widget.items,
onValueChanges: (values, valid) { onValueChanges: (values, valid, isBuilding) {
setState(() { if (isBuilding) {
this.values = values; this.values = values;
this.valid = valid; this.valid = valid;
}); } else {
setState(() {
this.values = values;
this.valid = valid;
});
}
}, },
defaultValues: widget.defaultValues) defaultValues: widget.defaultValues)
]), ]),

View File

@@ -16,7 +16,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
const String currentVersion = '0.7.2'; const String currentVersion = '0.7.5';
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

@@ -27,14 +27,9 @@ class GitHubStars implements MassAppUrlSource {
} }
return urlsWithDescriptions; return urlsWithDescriptions;
} else { } else {
if (res.headers['x-ratelimit-remaining'] == '0') { var gh = GitHub();
throw RateLimitError( gh.rateLimitErrorCheck(res);
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / throw getObtainiumHttpError(res);
60000000)
.round());
}
throw ObtainiumError('Unable to find user\'s starred repos');
} }
} }

View File

@@ -66,7 +66,7 @@ class _AddAppPageState extends State<AddAppPage> {
]) ])
] ]
], ],
onValueChanges: (values, valid) { onValueChanges: (values, valid, isBuilding) {
setState(() { setState(() {
userInput = values[0]; userInput = values[0];
var source = valid var source = valid
@@ -115,18 +115,23 @@ class _AddAppPageState extends State<AddAppPage> {
additionalData); additionalData);
await settingsProvider await settingsProvider
.getInstallPermission(); .getInstallPermission();
// ignore: use_build_context_synchronously // Only download the APK here if you need to for the package ID
var apkUrl = await appsProvider if (sourceProvider
.confirmApkUrl(app, context); .isTempId(app.id)) {
if (apkUrl == null) { // ignore: use_build_context_synchronously
throw ObtainiumError('Cancelled'); var apkUrl = await appsProvider
.confirmApkUrl(app, context);
if (apkUrl == null) {
throw ObtainiumError(
'Cancelled');
}
app.preferredApkIndex =
app.apkUrls.indexOf(apkUrl);
var downloadedApk =
await appsProvider
.downloadApp(app);
app.id = downloadedApk.appId;
} }
app.preferredApkIndex =
app.apkUrls.indexOf(apkUrl);
var downloadedApk =
await appsProvider
.downloadApp(app);
app.id = downloadedApk.appId;
if (appsProvider.apps if (appsProvider.apps
.containsKey(app.id)) { .containsKey(app.id)) {
throw ObtainiumError( throw ObtainiumError(
@@ -174,7 +179,7 @@ class _AddAppPageState extends State<AddAppPage> {
.additionalDataFormItems.isNotEmpty) .additionalDataFormItems.isNotEmpty)
GeneratedForm( GeneratedForm(
items: pickedSource!.additionalDataFormItems, items: pickedSource!.additionalDataFormItems,
onValueChanges: (values, valid) { onValueChanges: (values, valid, isBuilding) {
setState(() { setState(() {
additionalData = values; additionalData = values;
validAdditionalData = valid; validAdditionalData = valid;

View File

@@ -142,7 +142,7 @@ class _SettingsPageState extends State<SettingsPage> {
if (e.moreSourceSettingsFormItems.isNotEmpty) { if (e.moreSourceSettingsFormItems.isNotEmpty) {
return GeneratedForm( return GeneratedForm(
items: e.moreSourceSettingsFormItems.map((e) => [e]).toList(), items: e.moreSourceSettingsFormItems.map((e) => [e]).toList(),
onValueChanges: (values, valid) { onValueChanges: (values, valid, isBuilding) {
if (valid) { if (valid) {
for (var i = 0; i < values.length; i++) { for (var i = 0; i < values.length; i++) {
settingsProvider.setSettingString( settingsProvider.setSettingString(
@@ -264,27 +264,11 @@ class _SettingsPageState extends State<SettingsPage> {
if (logs.isEmpty) { if (logs.isEmpty) {
showError(ObtainiumError('No Logs'), context); showError(ObtainiumError('No Logs'), context);
} else { } else {
String logString =
logs.map((e) => e.toString()).join('\n\n');
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return GeneratedFormModal( return const LogsDialog();
title: 'Obtainium App Logs', });
items: const [],
defaultValues: const [],
message: logString,
initValid: true,
);
}).then((value) {
if (value != null) {
Share.share(
logs
.map((e) => e.toString())
.join('\n\n'),
subject: 'Obtainium App Logs');
}
});
} }
}); });
}, },
@@ -299,3 +283,71 @@ class _SettingsPageState extends State<SettingsPage> {
])); ]));
} }
} }
class LogsDialog extends StatefulWidget {
const LogsDialog({super.key});
@override
State<LogsDialog> createState() => _LogsDialogState();
}
class _LogsDialogState extends State<LogsDialog> {
String? logString;
List<int> days = [7, 5, 4, 3, 2, 1];
@override
Widget build(BuildContext context) {
var logsProvider = context.read<LogsProvider>();
void filterLogs(int days) {
logsProvider
.get(after: DateTime.now().subtract(Duration(days: days)))
.then((value) {
setState(() {
String l = value.map((e) => e.toString()).join('\n\n');
logString = l.isNotEmpty ? l : 'No Logs';
});
});
}
if (logString == null) {
filterLogs(days.first);
}
return AlertDialog(
scrollable: true,
title: const Text('Obtainium App Logs'),
content: Column(
children: [
DropdownButtonFormField(
value: days.first,
items: days
.map((e) => DropdownMenuItem(
value: e,
child: Text('$e Day${e == 1 ? '' : 's'}'),
))
.toList(),
onChanged: (d) {
filterLogs(d ?? 7);
}),
const SizedBox(
height: 32,
),
Text(logString ?? '')
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Close')),
TextButton(
onPressed: () {
Share.share(logString ?? '', subject: 'Obtainium App Logs');
Navigator.of(context).pop();
},
child: const Text('Share'))
],
);
}
}

View File

@@ -65,7 +65,9 @@ class AppsProvider with ChangeNotifier {
// Delete existing APKs // Delete existing APKs
(await getExternalStorageDirectory()) (await getExternalStorageDirectory())
?.listSync() ?.listSync()
.where((element) => element.path.endsWith('.apk')) .where((element) =>
element.path.endsWith('.apk') ||
element.path.endsWith('.apk.part'))
.forEach((apk) { .forEach((apk) {
apk.delete(); apk.delete();
}); });
@@ -73,38 +75,39 @@ class AppsProvider with ChangeNotifier {
} }
} }
downloadFile(String url, String fileName, Function? onProgress) async { downloadFile(String url, String fileName, Function? onProgress,
{bool useExisting = true}) async {
var destDir = (await getExternalStorageDirectory())!.path; var destDir = (await getExternalStorageDirectory())!.path;
StreamedResponse response = StreamedResponse response =
await Client().send(Request('GET', Uri.parse(url))); await Client().send(Request('GET', Uri.parse(url)));
File downloadedFile = File('$destDir/$fileName'); File downloadedFile = File('$destDir/$fileName');
if (!(downloadedFile.existsSync() && useExisting)) {
if (downloadedFile.existsSync()) { File tempDownloadedFile = File('${downloadedFile.path}.part');
downloadedFile.deleteSync(); if (tempDownloadedFile.existsSync()) {
} tempDownloadedFile.deleteSync();
var length = response.contentLength; }
var received = 0; var length = response.contentLength;
double? progress; var received = 0;
var sink = downloadedFile.openWrite(); double? progress;
var sink = tempDownloadedFile.openWrite();
await response.stream.map((s) { await response.stream.map((s) {
received += s.length; received += s.length;
progress = (length != null ? received / length * 100 : 30); progress = (length != null ? received / length * 100 : 30);
if (onProgress != null) {
onProgress(progress);
}
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (onProgress != null) { if (onProgress != null) {
onProgress(progress); onProgress(progress);
} }
return s; if (response.statusCode != 200) {
}).pipe(sink); tempDownloadedFile.deleteSync();
throw response.reasonPhrase ?? 'Unknown Error';
await sink.close(); }
progress = null; tempDownloadedFile.renameSync(downloadedFile.path);
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode != 200) {
downloadedFile.deleteSync();
throw response.reasonPhrase ?? 'Unknown Error';
} }
return downloadedFile; return downloadedFile;
} }
@@ -424,22 +427,28 @@ class AppsProvider with ChangeNotifier {
} }
loadingApps = true; loadingApps = true;
notifyListeners(); notifyListeners();
List<FileSystemEntity> appFiles = (await getAppsDir()) List<App> newApps = (await getAppsDir())
.listSync() .listSync()
.where((item) => item.path.toLowerCase().endsWith('.json')) .where((item) => item.path.toLowerCase().endsWith('.json'))
.map((e) => App.fromJson(jsonDecode(File(e.path).readAsStringSync())))
.toList(); .toList();
apps.clear(); var idsToDelete = apps.values
.map((e) => e.app.id)
.toSet()
.difference(newApps.map((e) => e.id).toSet());
for (var id in idsToDelete) {
apps.remove(id);
}
var sp = SourceProvider(); var sp = SourceProvider();
List<List<String>> errors = []; List<List<String>> errors = [];
for (int i = 0; i < appFiles.length; i++) { for (int i = 0; i < newApps.length; i++) {
App app = var info = await getInstalledInfo(newApps[i].id);
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
var info = await getInstalledInfo(app.id);
try { try {
sp.getSource(app.url); sp.getSource(newApps[i].url);
apps.putIfAbsent(app.id, () => AppInMemory(app, null, info)); apps.putIfAbsent(
newApps[i].id, () => AppInMemory(newApps[i], null, info));
} catch (e) { } catch (e) {
errors.add([app.id, app.name, e.toString()]); errors.add([newApps[i].id, newApps[i].name, e.toString()]);
} }
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {

View File

@@ -4,6 +4,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroid.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';
@@ -154,14 +155,23 @@ class AppSource {
throw NotImplementedError(); throw NotImplementedError();
} }
Future<String> apkUrlPrefetchModifier(String apkUrl) { Future<String> apkUrlPrefetchModifier(String apkUrl) async {
throw NotImplementedError(); return apkUrl;
} }
bool canSearch = false; bool canSearch = false;
Future<Map<String, String>> search(String query) { Future<Map<String, String>> search(String query) {
throw NotImplementedError(); throw NotImplementedError();
} }
String? tryInferringAppId(String standardUrl) {
return null;
}
}
ObtainiumError getObtainiumHttpError(Response res) {
return ObtainiumError(
res.reasonPhrase ?? 'Error ${res.statusCode.toString()}');
} }
abstract class MassAppUrlSource { abstract class MassAppUrlSource {
@@ -234,7 +244,9 @@ class SourceProvider {
APKDetails apk = APKDetails apk =
await source.getLatestAPKDetails(standardUrl, additionalData); await source.getLatestAPKDetails(standardUrl, additionalData);
return App( return App(
id ?? generateTempID(names, source), id ??
source.tryInferringAppId(standardUrl) ??
generateTempID(names, source),
standardUrl, standardUrl,
names.author[0].toUpperCase() + names.author.substring(1), names.author[0].toUpperCase() + names.author.substring(1),
name.trim().isNotEmpty name.trim().isNotEmpty

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.7.2+58 # When changing this, update the tag in main() accordingly version: 0.7.5+61 # 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'