mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-25 02:19:41 +02:00
Adds Source-specific options + other changes (#26)
* Started work on dynamic forms * dynamic form progress (switch doesn't work) * dynamic forms work * Gen. form improvements, source specific data (untested) * Gen form bugfix * Removed redundant generated modal code * Added custom validators to gen. forms * Progress on source options (incomplete), gen form bugfixes * Tweaks, more * More * Progress * Changed a default * Additional options done!
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class FDroid implements AppSource {
|
||||
@@ -11,13 +12,14 @@ class FDroid implements AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/packages/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw notValidURL;
|
||||
throw notValidURL(runtimeType.toString());
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData) async {
|
||||
Response res = await get(Uri.parse(standardUrl));
|
||||
if (res.statusCode == 200) {
|
||||
var latestReleaseDiv =
|
||||
@@ -46,4 +48,10 @@ class FDroid implements AppSource {
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
||||
}
|
||||
|
||||
@override
|
||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||
|
||||
@override
|
||||
List<String> additionalDataDefaults = [];
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class GitHub implements AppSource {
|
||||
@@ -11,47 +12,68 @@ class GitHub implements AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw notValidURL;
|
||||
throw notValidURL(runtimeType.toString());
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData) async {
|
||||
var includePrereleases =
|
||||
additionalData.isNotEmpty && additionalData[0] == "true";
|
||||
var fallbackToOlderReleases =
|
||||
additionalData.length >= 2 && additionalData[1] == "true";
|
||||
var regexFilter = additionalData.length >= 3 && additionalData[2].isNotEmpty
|
||||
? additionalData[2]
|
||||
: null;
|
||||
Response res = await get(Uri.parse(
|
||||
'https://api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
||||
if (res.statusCode == 200) {
|
||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||
// Right now, the latest non-prerelease version is picked
|
||||
// If none exists, the latest prerelease version is picked
|
||||
// In the future, the user could be given a choice
|
||||
var nonPrereleaseReleases =
|
||||
releases.where((element) => element['prerelease'] != true).toList();
|
||||
var latestRelease = nonPrereleaseReleases.isNotEmpty
|
||||
? nonPrereleaseReleases[0]
|
||||
: releases.isNotEmpty
|
||||
? releases[0]
|
||||
: null;
|
||||
if (latestRelease == null) {
|
||||
|
||||
List<String> getReleaseAPKUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)
|
||||
?.map((e) {
|
||||
return e['browser_download_url'] != null
|
||||
? e['browser_download_url'] as String
|
||||
: '';
|
||||
})
|
||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
dynamic targetRelease;
|
||||
|
||||
for (int i = 0; i < releases.length; i++) {
|
||||
if (!fallbackToOlderReleases && i > 0) break;
|
||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||
continue;
|
||||
}
|
||||
if (regexFilter != null &&
|
||||
!RegExp(regexFilter)
|
||||
.hasMatch((releases[i]['name'] as String).trim())) {
|
||||
continue;
|
||||
}
|
||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||
if (apkUrls.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
targetRelease = releases[i];
|
||||
targetRelease['apkUrls'] = apkUrls;
|
||||
break;
|
||||
}
|
||||
if (targetRelease == null) {
|
||||
throw couldNotFindReleases;
|
||||
}
|
||||
List<dynamic>? assets = latestRelease['assets'];
|
||||
List<String>? apkUrlList = assets
|
||||
?.map((e) {
|
||||
return e['browser_download_url'] != null
|
||||
? e['browser_download_url'] as String
|
||||
: '';
|
||||
})
|
||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
if (apkUrlList == null || apkUrlList.isEmpty) {
|
||||
if ((targetRelease['apkUrls'] as List<String>).isEmpty) {
|
||||
throw noAPKFound;
|
||||
}
|
||||
String? version = latestRelease['tag_name'];
|
||||
String? version = targetRelease['tag_name'];
|
||||
if (version == null) {
|
||||
throw couldNotFindLatestVersion;
|
||||
}
|
||||
return APKDetails(version, apkUrlList);
|
||||
return APKDetails(version, targetRelease['apkUrls']);
|
||||
} else {
|
||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||
throw 'Rate limit reached - try again in ${(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000).toString()} minutes';
|
||||
@@ -67,4 +89,34 @@ class GitHub implements AppSource {
|
||||
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
||||
return AppNames(names[0], names[1]);
|
||||
}
|
||||
|
||||
@override
|
||||
List<List<GeneratedFormItem>> additionalDataFormItems = [
|
||||
[GeneratedFormItem(label: "Include prereleases", type: FormItemType.bool)],
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: "Fallback to older releases", type: FormItemType.bool)
|
||||
],
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: "Filter Release Titles by Regular Expression",
|
||||
type: FormItemType.string,
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
RegExp(value);
|
||||
} catch (e) {
|
||||
return "Invalid regular expression";
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
];
|
||||
|
||||
@override
|
||||
List<String> additionalDataDefaults = ["true", "true", ""];
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class GitLab implements AppSource {
|
||||
@@ -12,13 +13,14 @@ class GitLab implements AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw notValidURL;
|
||||
throw notValidURL(runtimeType.toString());
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData) async {
|
||||
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
||||
if (res.statusCode == 200) {
|
||||
var standardUri = Uri.parse(standardUrl);
|
||||
@@ -60,4 +62,10 @@ class GitLab implements AppSource {
|
||||
// Same as GitHub
|
||||
return GitHub().getAppNames(standardUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||
|
||||
@override
|
||||
List<String> additionalDataDefaults = [];
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class IzzyOnDroid implements AppSource {
|
||||
@@ -11,13 +12,14 @@ class IzzyOnDroid implements AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw notValidURL;
|
||||
throw notValidURL(runtimeType.toString());
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData) async {
|
||||
Response res = await get(Uri.parse(standardUrl));
|
||||
if (res.statusCode == 200) {
|
||||
var parsedHtml = parse(res.body);
|
||||
@@ -54,4 +56,10 @@ class IzzyOnDroid implements AppSource {
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last);
|
||||
}
|
||||
|
||||
@override
|
||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||
|
||||
@override
|
||||
List<String> additionalDataDefaults = [];
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Mullvad implements AppSource {
|
||||
@@ -11,13 +12,14 @@ class Mullvad implements AppSource {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw notValidURL;
|
||||
throw notValidURL(runtimeType.toString());
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData) async {
|
||||
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
|
||||
if (res.statusCode == 200) {
|
||||
var version = parse(res.body)
|
||||
@@ -40,4 +42,10 @@ class Mullvad implements AppSource {
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
return AppNames('Mullvad-VPN', 'Mullvad-VPN');
|
||||
}
|
||||
|
||||
@override
|
||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||
|
||||
@override
|
||||
List<String> additionalDataDefaults = [];
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Signal implements AppSource {
|
||||
@@ -12,7 +13,8 @@ class Signal implements AppSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData) async {
|
||||
Response res =
|
||||
await get(Uri.parse('https://updates.$host/android/latest.json'));
|
||||
if (res.statusCode == 200) {
|
||||
@@ -33,4 +35,10 @@ class Signal implements AppSource {
|
||||
|
||||
@override
|
||||
AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal');
|
||||
|
||||
@override
|
||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||
|
||||
@override
|
||||
List<String> additionalDataDefaults = [];
|
||||
}
|
||||
|
175
lib/components/generated_form.dart
Normal file
175
lib/components/generated_form.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum FormItemType { string, bool }
|
||||
|
||||
typedef OnValueChanges = void Function(List<String> values, bool valid);
|
||||
|
||||
class GeneratedFormItem {
|
||||
late String label;
|
||||
late FormItemType type;
|
||||
late bool required;
|
||||
late int max;
|
||||
late List<String? Function(String? value)> additionalValidators;
|
||||
|
||||
GeneratedFormItem(
|
||||
{this.label = "Input",
|
||||
this.type = FormItemType.string,
|
||||
this.required = true,
|
||||
this.max = 1,
|
||||
this.additionalValidators = const []});
|
||||
}
|
||||
|
||||
class GeneratedForm extends StatefulWidget {
|
||||
const GeneratedForm(
|
||||
{super.key,
|
||||
required this.items,
|
||||
required this.onValueChanges,
|
||||
required this.defaultValues});
|
||||
|
||||
final List<List<GeneratedFormItem>> items;
|
||||
final OnValueChanges onValueChanges;
|
||||
final List<String> defaultValues;
|
||||
|
||||
@override
|
||||
State<GeneratedForm> createState() => _GeneratedFormState();
|
||||
}
|
||||
|
||||
class _GeneratedFormState extends State<GeneratedForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late List<List<String>> values;
|
||||
late List<List<Widget>> formInputs;
|
||||
List<List<Widget>> rows = [];
|
||||
|
||||
// If any value changes, call this to update the parent with value and validity
|
||||
void someValueChanged() {
|
||||
List<String> returnValues = [];
|
||||
var valid = true;
|
||||
for (int r = 0; r < values.length; r++) {
|
||||
for (int i = 0; i < values[r].length; i++) {
|
||||
returnValues.add(values[r][i]);
|
||||
if (formInputs[r][i] is TextFormField) {
|
||||
valid = valid &&
|
||||
((formInputs[r][i].key as GlobalKey<FormFieldState>)
|
||||
.currentState
|
||||
?.isValid ??
|
||||
false);
|
||||
}
|
||||
}
|
||||
}
|
||||
widget.onValueChanges(returnValues, valid);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize form values as all empty
|
||||
int j = 0;
|
||||
values = widget.items
|
||||
.map((row) => row.map((e) {
|
||||
return j < widget.defaultValues.length
|
||||
? widget.defaultValues[j++]
|
||||
: "";
|
||||
}).toList())
|
||||
.toList();
|
||||
|
||||
// Dynamically create form inputs
|
||||
formInputs = widget.items.asMap().entries.map((row) {
|
||||
return row.value.asMap().entries.map((e) {
|
||||
if (e.value.type == FormItemType.string) {
|
||||
final formFieldKey = GlobalKey<FormFieldState>();
|
||||
return TextFormField(
|
||||
key: formFieldKey,
|
||||
initialValue: values[row.key][e.key],
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
values[row.key][e.key] = value;
|
||||
someValueChanged();
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
helperText: e.value.label + (e.value.required ? " *" : "")),
|
||||
minLines: e.value.max <= 1 ? null : e.value.max,
|
||||
maxLines: e.value.max <= 1 ? 1 : e.value.max,
|
||||
validator: (value) {
|
||||
if (e.value.required && (value == null || value.trim().isEmpty)) {
|
||||
return '${e.value.label} (required)';
|
||||
}
|
||||
for (var validator in e.value.additionalValidators) {
|
||||
String? result = validator(value);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Container(); // Some input types added in build
|
||||
}
|
||||
}).toList();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
for (var r = 0; r < formInputs.length; r++) {
|
||||
for (var e = 0; e < formInputs[r].length; e++) {
|
||||
if (widget.items[r][e].type == FormItemType.bool) {
|
||||
formInputs[r][e] = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(widget.items[r][e].label),
|
||||
Switch(
|
||||
value: values[r][e] == "true",
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
values[r][e] = value ? "true" : "";
|
||||
someValueChanged();
|
||||
});
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.clear();
|
||||
formInputs.asMap().entries.forEach((rowInputs) {
|
||||
if (rowInputs.key > 0) {
|
||||
rows.add([
|
||||
SizedBox(
|
||||
height: widget.items[rowInputs.key][0].type == FormItemType.bool &&
|
||||
widget.items[rowInputs.key - 1][0].type ==
|
||||
FormItemType.string
|
||||
? 25
|
||||
: 8,
|
||||
)
|
||||
]);
|
||||
}
|
||||
List<Widget> rowItems = [];
|
||||
rowInputs.value.asMap().entries.forEach((rowInput) {
|
||||
if (rowInput.key > 0) {
|
||||
rowItems.add(const SizedBox(
|
||||
width: 20,
|
||||
));
|
||||
}
|
||||
rowItems.add(Expanded(child: rowInput.value));
|
||||
});
|
||||
rows.add(rowItems);
|
||||
});
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
...rows.map((row) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [...row.map((e) => e)],
|
||||
))
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
@@ -1,61 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class GeneratedFormItem {
|
||||
late String message;
|
||||
late bool required;
|
||||
late int lines;
|
||||
|
||||
GeneratedFormItem(this.message, this.required, this.lines);
|
||||
}
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
|
||||
class GeneratedFormModal extends StatefulWidget {
|
||||
const GeneratedFormModal(
|
||||
{super.key, required this.title, required this.items});
|
||||
{super.key,
|
||||
required this.title,
|
||||
required this.items,
|
||||
required this.defaultValues});
|
||||
|
||||
final String title;
|
||||
final List<GeneratedFormItem> items;
|
||||
final List<List<GeneratedFormItem>> items;
|
||||
final List<String> defaultValues;
|
||||
|
||||
@override
|
||||
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
||||
}
|
||||
|
||||
class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final urlInputController = TextEditingController();
|
||||
List<String> values = [];
|
||||
bool valid = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formInputs = widget.items.map((e) {
|
||||
final controller = TextEditingController();
|
||||
return [
|
||||
controller,
|
||||
TextFormField(
|
||||
decoration: InputDecoration(helperText: e.message),
|
||||
controller: controller,
|
||||
minLines: e.lines <= 1 ? null : e.lines,
|
||||
maxLines: e.lines <= 1 ? 1 : e.lines,
|
||||
validator: e.required
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '${e.message} (required)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
)
|
||||
];
|
||||
}).toList();
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(widget.title),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [...formInputs.map((e) => e[1] as Widget)],
|
||||
)),
|
||||
content: GeneratedForm(
|
||||
items: widget.items,
|
||||
onValueChanges: (values, valid) {
|
||||
setState(() {
|
||||
this.values = values;
|
||||
this.valid = valid;
|
||||
});
|
||||
},
|
||||
defaultValues: widget.defaultValues),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -63,18 +42,16 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||
},
|
||||
child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() == true) {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(formInputs
|
||||
.map((e) => (e[0] as TextEditingController).value.text)
|
||||
.toList());
|
||||
}
|
||||
},
|
||||
onPressed: !valid
|
||||
? null
|
||||
: () {
|
||||
if (valid) {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(values);
|
||||
}
|
||||
},
|
||||
child: const Text('Continue'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add support for larger textarea so this can be used for text/json imports
|
@@ -59,7 +59,7 @@ void main() async {
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => AppsProvider(
|
||||
shouldLoadApps: true,
|
||||
shouldCheckUpdatesAfterLoad: true,
|
||||
shouldCheckUpdatesAfterLoad: false,
|
||||
shouldDeleteAPKs: true)),
|
||||
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
||||
Provider(create: (context) => NotificationsProvider())
|
||||
@@ -103,7 +103,8 @@ class MyApp extends StatelessWidget {
|
||||
currentReleaseTag,
|
||||
currentReleaseTag,
|
||||
[],
|
||||
0));
|
||||
0,
|
||||
["true"]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/pages/app.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
@@ -16,10 +17,13 @@ class AddAppPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddAppPageState extends State<AddAppPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final urlInputController = TextEditingController();
|
||||
bool gettingAppInfo = false;
|
||||
|
||||
String userInput = "";
|
||||
AppSource? pickedSource;
|
||||
List<String> additionalData = [];
|
||||
bool validAdditionalData = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
@@ -28,103 +32,147 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
const CustomAppBar(title: 'Add App'),
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText:
|
||||
'https://github.com/Author/Project',
|
||||
helperText: 'Enter the App source URL'),
|
||||
controller: urlInputController,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
Uri.tryParse(value) == null) {
|
||||
return 'Please enter a supported source URL';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: gettingAppInfo
|
||||
? null
|
||||
: () {
|
||||
HapticFeedback.selectionClick();
|
||||
if (_formKey.currentState!
|
||||
.validate()) {
|
||||
setState(() {
|
||||
gettingAppInfo = true;
|
||||
});
|
||||
sourceProvider
|
||||
.getApp(urlInputController
|
||||
.value.text)
|
||||
.then((app) {
|
||||
var appsProvider =
|
||||
context.read<AppsProvider>();
|
||||
var settingsProvider = context
|
||||
.read<SettingsProvider>();
|
||||
if (appsProvider.apps
|
||||
.containsKey(app.id)) {
|
||||
throw 'App already added';
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: "App Source Url",
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
try {
|
||||
sourceProvider
|
||||
.getSource(value ?? "")
|
||||
.standardizeURL(
|
||||
makeUrlHttps(
|
||||
value ?? ""));
|
||||
} catch (e) {
|
||||
return e is String
|
||||
? e
|
||||
: "Error";
|
||||
}
|
||||
settingsProvider
|
||||
.getInstallPermission()
|
||||
.then((_) {
|
||||
appsProvider
|
||||
.saveApp(app)
|
||||
.then((_) {
|
||||
urlInputController.clear();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
AppPage(
|
||||
appId:
|
||||
app.id)));
|
||||
});
|
||||
});
|
||||
}).catchError((e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text(e.toString())),
|
||||
);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
gettingAppInfo = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('Add'),
|
||||
),
|
||||
),
|
||||
],
|
||||
return null;
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid) {
|
||||
setState(() {
|
||||
userInput = values[0];
|
||||
var source = valid
|
||||
? sourceProvider.getSource(userInput)
|
||||
: null;
|
||||
if (pickedSource != source) {
|
||||
pickedSource = source;
|
||||
additionalData = [];
|
||||
validAdditionalData = source != null
|
||||
? sourceProvider
|
||||
.doesSourceHaveRequiredAdditionalData(
|
||||
source)
|
||||
: true;
|
||||
}
|
||||
});
|
||||
},
|
||||
defaultValues: const [])),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: gettingAppInfo ||
|
||||
pickedSource == null ||
|
||||
(pickedSource!.additionalDataFormItems
|
||||
.isNotEmpty &&
|
||||
!validAdditionalData)
|
||||
? null
|
||||
: () {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() {
|
||||
gettingAppInfo = true;
|
||||
});
|
||||
sourceProvider
|
||||
.getApp(pickedSource!, userInput,
|
||||
additionalData)
|
||||
.then((app) {
|
||||
var appsProvider =
|
||||
context.read<AppsProvider>();
|
||||
var settingsProvider =
|
||||
context.read<SettingsProvider>();
|
||||
if (appsProvider.apps
|
||||
.containsKey(app.id)) {
|
||||
throw 'App already added';
|
||||
}
|
||||
settingsProvider
|
||||
.getInstallPermission()
|
||||
.then((_) {
|
||||
appsProvider.saveApp(app).then((_) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
AppPage(
|
||||
appId: app.id)));
|
||||
});
|
||||
});
|
||||
}).catchError((e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
gettingAppInfo = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
child: const Text('Add'))
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 64,
|
||||
),
|
||||
if (pickedSource != null &&
|
||||
(pickedSource!.additionalDataFormItems.isNotEmpty))
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Additional Options for ${pickedSource?.runtimeType}',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
GeneratedForm(
|
||||
items: pickedSource!.additionalDataFormItems,
|
||||
onValueChanges: (values, valid) {
|
||||
setState(() {
|
||||
additionalData = values;
|
||||
validAdditionalData = valid;
|
||||
});
|
||||
},
|
||||
defaultValues:
|
||||
pickedSource!.additionalDataDefaults)
|
||||
],
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// const SizedBox(
|
||||
// height: 48,
|
||||
// ),
|
||||
const Text(
|
||||
'Supported Sources:',
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
@@ -145,14 +193,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
fontStyle: FontStyle.italic),
|
||||
)))
|
||||
.toList()
|
||||
]),
|
||||
if (gettingAppInfo)
|
||||
const LinearProgressIndicator()
|
||||
else
|
||||
Container(),
|
||||
],
|
||||
)),
|
||||
))
|
||||
])),
|
||||
])),
|
||||
)
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -20,7 +23,9 @@ class _AppPageState extends State<AppPage> {
|
||||
Widget build(BuildContext context) {
|
||||
var appsProvider = context.watch<AppsProvider>();
|
||||
var settingsProvider = context.watch<SettingsProvider>();
|
||||
var sourceProvider = SourceProvider();
|
||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
||||
if (app?.app.installedVersion != null) {
|
||||
appsProvider.getUpdate(app!.app.id);
|
||||
}
|
||||
@@ -159,6 +164,37 @@ class _AppPageState extends State<AppPage> {
|
||||
},
|
||||
tooltip: 'Mark as Not Installed',
|
||||
icon: const Icon(Icons.no_cell_outlined)),
|
||||
if (source != null &&
|
||||
source.additionalDataFormItems.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: 'Additional Options',
|
||||
items: source.additionalDataFormItems,
|
||||
defaultValues:
|
||||
source.additionalDataDefaults);
|
||||
}).then((values) {
|
||||
if (app != null && values != null) {
|
||||
var changedApp = app.app;
|
||||
changedApp.additionalData = values;
|
||||
sourceProvider
|
||||
.getApp(source, changedApp.url,
|
||||
changedApp.additionalData)
|
||||
.then((finalChangedApp) {
|
||||
appsProvider.saveApp(finalChangedApp);
|
||||
}).catchError((e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.settings)),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
|
@@ -5,6 +5,7 @@ import 'package:obtainium/pages/add_app.dart';
|
||||
import 'package:obtainium/pages/apps.dart';
|
||||
import 'package:obtainium/pages/import_export.dart';
|
||||
import 'package:obtainium/pages/settings.dart';
|
||||
import 'package:obtainium/pages/test_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
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/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
@@ -167,9 +168,34 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
return GeneratedFormModal(
|
||||
title: 'Import from URL List',
|
||||
items: [
|
||||
GeneratedFormItem(
|
||||
'App URL List', true, 7)
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'App URL List',
|
||||
max: 7,
|
||||
additionalValidators: [
|
||||
(String? value) {
|
||||
if (value != null &&
|
||||
value.isNotEmpty) {
|
||||
var lines = value
|
||||
.trim()
|
||||
.split('\n');
|
||||
for (int i = 0;
|
||||
i < lines.length;
|
||||
i++) {
|
||||
try {
|
||||
sourceProvider
|
||||
.getSource(
|
||||
lines[i]);
|
||||
} catch (e) {
|
||||
return 'Line ${i + 1}: $e';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
defaultValues: const [],
|
||||
);
|
||||
}).then((values) {
|
||||
if (values != null) {
|
||||
@@ -226,16 +252,17 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
builder:
|
||||
(BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title:
|
||||
'Import ${source.name}',
|
||||
items: source
|
||||
.requiredArgs
|
||||
.map((e) =>
|
||||
title:
|
||||
'Import ${source.name}',
|
||||
items: source
|
||||
.requiredArgs
|
||||
.map((e) => [
|
||||
GeneratedFormItem(
|
||||
e,
|
||||
true,
|
||||
1))
|
||||
.toList());
|
||||
label: e)
|
||||
])
|
||||
.toList(),
|
||||
defaultValues: const [],
|
||||
);
|
||||
}).then((values) {
|
||||
if (values != null) {
|
||||
source
|
||||
|
53
lib/pages/test_page.dart
Normal file
53
lib/pages/test_page.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
|
||||
class TestPage extends StatefulWidget {
|
||||
const TestPage({super.key});
|
||||
|
||||
@override
|
||||
State<TestPage> createState() => _TestPageState();
|
||||
}
|
||||
|
||||
class _TestPageState extends State<TestPage> {
|
||||
List<String?>? sourceSpecificData;
|
||||
bool valid = false;
|
||||
|
||||
List<List<GeneratedFormItem>> sourceSpecificInputs = [
|
||||
[GeneratedFormItem(label: 'Test Item 1')],
|
||||
[
|
||||
GeneratedFormItem(label: 'Test Item 2', required: false),
|
||||
GeneratedFormItem(label: 'Test Item 3')
|
||||
],
|
||||
[GeneratedFormItem(label: 'Test Item 4', type: FormItemType.bool)]
|
||||
];
|
||||
|
||||
List<String> defaultInputValues = ["ABC"];
|
||||
|
||||
void onSourceSpecificDataChanges(
|
||||
List<String?> valuesFromForm, bool formValid) {
|
||||
setState(() {
|
||||
sourceSpecificData = valuesFromForm;
|
||||
valid = formValid;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Test Page')),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(children: [
|
||||
GeneratedForm(
|
||||
items: sourceSpecificInputs,
|
||||
onValueChanges: onSourceSpecificDataChanges,
|
||||
defaultValues: defaultInputValues,
|
||||
),
|
||||
...(sourceSpecificData != null
|
||||
? (sourceSpecificData as List<String?>)
|
||||
.map((e) => Text(e ?? ""))
|
||||
: [Container()])
|
||||
])));
|
||||
}
|
||||
}
|
@@ -228,7 +228,11 @@ class AppsProvider with ChangeNotifier {
|
||||
|
||||
Future<App?> getUpdate(String appId) async {
|
||||
App? currentApp = apps[appId]!.app;
|
||||
App newApp = await SourceProvider().getApp(currentApp.url);
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
App newApp = await sourceProvider.getApp(
|
||||
sourceProvider.getSource(currentApp.url),
|
||||
currentApp.url,
|
||||
currentApp.additionalData);
|
||||
if (newApp.latestVersion != currentApp.latestVersion) {
|
||||
newApp.installedVersion = currentApp.installedVersion;
|
||||
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||
|
@@ -10,6 +10,7 @@ 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/components/generated_form.dart';
|
||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||
|
||||
class AppNames {
|
||||
@@ -35,8 +36,17 @@ class App {
|
||||
late String latestVersion;
|
||||
List<String> apkUrls = [];
|
||||
late int preferredApkIndex;
|
||||
App(this.id, this.url, this.author, this.name, this.installedVersion,
|
||||
this.latestVersion, this.apkUrls, this.preferredApkIndex);
|
||||
late List<String> additionalData;
|
||||
App(
|
||||
this.id,
|
||||
this.url,
|
||||
this.author,
|
||||
this.name,
|
||||
this.installedVersion,
|
||||
this.latestVersion,
|
||||
this.apkUrls,
|
||||
this.preferredApkIndex,
|
||||
this.additionalData);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -44,19 +54,21 @@ class App {
|
||||
}
|
||||
|
||||
factory App.fromJson(Map<String, dynamic> json) => App(
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
json['latestVersion'] as String,
|
||||
List<String>.from(jsonDecode(json['apkUrls'])),
|
||||
json['preferredApkIndex'] == null
|
||||
? 0
|
||||
: json['preferredApkIndex'] as int,
|
||||
);
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
json['latestVersion'] as String,
|
||||
json['apkUrls'] == null
|
||||
? []
|
||||
: List<String>.from(jsonDecode(json['apkUrls'])),
|
||||
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
|
||||
json['additionalData'] == null
|
||||
? SourceProvider().getSource(json['url']).additionalDataDefaults
|
||||
: List<String>.from(jsonDecode(json['additionalData'])));
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
@@ -66,7 +78,8 @@ class App {
|
||||
'installedVersion': installedVersion,
|
||||
'latestVersion': latestVersion,
|
||||
'apkUrls': jsonEncode(apkUrls),
|
||||
'preferredApkIndex': preferredApkIndex
|
||||
'preferredApkIndex': preferredApkIndex,
|
||||
'additionalData': jsonEncode(additionalData)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,10 +89,24 @@ escapeRegEx(String s) {
|
||||
});
|
||||
}
|
||||
|
||||
const String couldNotFindReleases = 'Unable to fetch release info';
|
||||
makeUrlHttps(String url) {
|
||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||
url.toLowerCase().indexOf('https://') != 0) {
|
||||
url = 'https://$url';
|
||||
}
|
||||
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
||||
url = 'https://${url.substring(12)}';
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
const String couldNotFindReleases = 'Could not find a suitable release';
|
||||
const String couldNotFindLatestVersion =
|
||||
'Could not determine latest release version';
|
||||
const String notValidURL = 'Not a valid URL';
|
||||
String notValidURL(String sourceName) {
|
||||
return 'Not a valid $sourceName App URL';
|
||||
}
|
||||
|
||||
const String noAPKFound = 'No APK found';
|
||||
|
||||
List<String> getLinksFromParsedHTML(
|
||||
@@ -96,8 +123,12 @@ List<String> getLinksFromParsedHTML(
|
||||
abstract class AppSource {
|
||||
late String host;
|
||||
String standardizeURL(String url);
|
||||
Future<APKDetails> getLatestAPKDetails(String standardUrl);
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl, List<String> additionalData);
|
||||
AppNames getAppNames(String standardUrl);
|
||||
late List<List<GeneratedFormItem>> additionalDataFormItems;
|
||||
late List<String>
|
||||
additionalDataDefaults; // TODO: Make these integrate into generated form
|
||||
}
|
||||
|
||||
abstract class MassAppSource {
|
||||
@@ -121,6 +152,7 @@ class SourceProvider {
|
||||
List<MassAppSource> massSources = [GitHubStars()];
|
||||
|
||||
AppSource getSource(String url) {
|
||||
url = makeUrlHttps(url);
|
||||
AppSource? source;
|
||||
for (var s in sources) {
|
||||
if (url.toLowerCase().contains('://${s.host}')) {
|
||||
@@ -134,18 +166,23 @@ class SourceProvider {
|
||||
return source;
|
||||
}
|
||||
|
||||
Future<App> getApp(String url) async {
|
||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||
url.toLowerCase().indexOf('https://') != 0) {
|
||||
url = 'https://$url';
|
||||
bool doesSourceHaveRequiredAdditionalData(AppSource source) {
|
||||
for (var row in source.additionalDataFormItems) {
|
||||
for (var element in row) {
|
||||
if (element.required) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
||||
url = 'https://${url.substring(12)}';
|
||||
}
|
||||
AppSource source = getSource(url);
|
||||
String standardUrl = source.standardizeURL(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<App> getApp(
|
||||
AppSource source, String url, List<String> additionalData) async {
|
||||
String standardUrl = source.standardizeURL(makeUrlHttps(url));
|
||||
AppNames names = source.getAppNames(standardUrl);
|
||||
APKDetails apk = await source.getLatestAPKDetails(standardUrl);
|
||||
APKDetails apk =
|
||||
await source.getLatestAPKDetails(standardUrl, additionalData);
|
||||
return App(
|
||||
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}',
|
||||
standardUrl,
|
||||
@@ -154,7 +191,8 @@ class SourceProvider {
|
||||
null,
|
||||
apk.version,
|
||||
apk.apkUrls,
|
||||
apk.apkUrls.length - 1);
|
||||
apk.apkUrls.length - 1,
|
||||
additionalData);
|
||||
}
|
||||
|
||||
/// Returns a length 2 list, where the first element is a list of Apps and
|
||||
@@ -164,7 +202,8 @@ class SourceProvider {
|
||||
Map<String, dynamic> errors = {};
|
||||
for (var url in urls) {
|
||||
try {
|
||||
apps.add(await getApp(url));
|
||||
var source = getSource(url);
|
||||
apps.add(await getApp(source, url, source.additionalDataDefaults));
|
||||
} catch (e) {
|
||||
errors.addAll(<String, dynamic>{url: e});
|
||||
}
|
||||
|
Reference in New Issue
Block a user