Skip to content

Commit

Permalink
Merge pull request #1696 from ImranR98/dev
Browse files Browse the repository at this point in the history
- Bugfix: Pull to refresh not working with few apps (#1680)
- Add third-party F-Droid repo search to main search menu (#1681)
- Added autocomplete for F-Droid repos (#1681)
- Bugfix: Missing request headers for direct APK link apps (#1688)
- Release asset download confirmation even for single choice (#1694)
- Add a less obvious touch target to highlights (#1694)
  • Loading branch information
ImranR98 authored Jun 29, 2024
2 parents 6c806a4 + 3b49451 commit cd153e7
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 89 deletions.
8 changes: 8 additions & 0 deletions lib/app_sources/directAPKLink.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ class DirectAPKLink extends AppSource {
];
}

@override
Future<Map<String, String>?> getRequestHeaders(
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) {
return html.getRequestHeaders(additionalSettings,
forAPKDownload: forAPKDownload);
}

@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Expand Down
24 changes: 23 additions & 1 deletion lib/app_sources/fdroidrepo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class FDroidRepo extends AppSource {
FDroidRepo() {
name = tr('fdroidThirdPartyRepo');
canSearch = true;
excludeFromMassSearch = true;
includeAdditionalOptsInMainSearch = true;
neverAutoSelect = true;
showReleaseDateAsVersionToggle = true;

Expand Down Expand Up @@ -86,6 +86,27 @@ class FDroidRepo extends AppSource {
}
}

@override
void runOnAddAppInputChange(String userInput) {
this.additionalSourceAppSpecificSettingFormItems =
this.additionalSourceAppSpecificSettingFormItems.map((row) {
row = row.map((item) {
if (item.key == 'appIdOrName') {
try {
var appId = Uri.parse(userInput).queryParameters['appId'];
if (appId != null && item is GeneratedFormTextField) {
item.required = false;
}
} catch (e) {
//
}
}
return item;
}).toList();
return row;
}).toList();
}

@override
App endOfGetAppChanges(App app) {
var uri = Uri.parse(app.url);
Expand Down Expand Up @@ -142,6 +163,7 @@ class FDroidRepo extends AppSource {
if (appIdOrName == null) {
throw NoReleasesError();
}
additionalSettings['appIdOrName'] = appIdOrName;
var res =
await sourceRequestWithURLVariants(standardUrl, additionalSettings);
if (res.statusCode == 200) {
Expand Down
11 changes: 7 additions & 4 deletions lib/app_sources/html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,13 @@ class HTML extends AppSource {
additionalSettings['versionExtractWholePage'] == true
? versionExtractionWholePageString
: relDecoded);
version ??=
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
? rel.hashCode.toString()
: (await checkPartialDownloadHashDynamic(rel)).toString();
version ??= additionalSettings['defaultPseudoVersioningMethod'] ==
'APKLinkHash'
? rel.hashCode.toString()
: (await checkPartialDownloadHashDynamic(rel,
headers: await getRequestHeaders(additionalSettings,
forAPKDownload: true)))
.toString();
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
AppNames(uri.host, tr('app')));
}
Expand Down
81 changes: 54 additions & 27 deletions lib/components/generated_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';

abstract class GeneratedFormItem {
late String key;
Expand All @@ -28,6 +29,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
late String? hint;
late bool password;
late TextInputType? textInputType;
late List<String>? autoCompleteOptions;

GeneratedFormTextField(super.key,
{super.label,
Expand All @@ -39,7 +41,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
this.max = 1,
this.hint,
this.password = false,
this.textInputType});
this.textInputType,
this.autoCompleteOptions});

@override
String ensureType(val) {
Expand Down Expand Up @@ -274,38 +277,62 @@ class _GeneratedFormState extends State<GeneratedForm> {
var formItem = e.value;
if (formItem is GeneratedFormTextField) {
final formFieldKey = GlobalKey<FormFieldState>();
return TextFormField(
keyboardType: formItem.textInputType,
obscureText: formItem.password,
autocorrect: !formItem.password,
enableSuggestions: !formItem.password,
key: formFieldKey,
initialValue: values[formItem.key],
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (value) {
var ctrl = TextEditingController(text: values[formItem.key]);
return TypeAheadField<String>(
controller: ctrl,
builder: (context, controller, focusNode) {
return TextFormField(
controller: ctrl,
focusNode: focusNode,
keyboardType: formItem.textInputType,
obscureText: formItem.password,
autocorrect: !formItem.password,
enableSuggestions: !formItem.password,
key: formFieldKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (value) {
setState(() {
values[formItem.key] = value;
someValueChanged();
});
},
decoration: InputDecoration(
helperText:
formItem.label + (formItem.required ? ' *' : ''),
hintText: formItem.hint),
minLines: formItem.max <= 1 ? null : formItem.max,
maxLines: formItem.max <= 1 ? 1 : formItem.max,
validator: (value) {
if (formItem.required &&
(value == null || value.trim().isEmpty)) {
return '${formItem.label} ${tr('requiredInBrackets')}';
}
for (var validator in formItem.additionalValidators) {
String? result = validator(value);
if (result != null) {
return result;
}
}
return null;
},
);
},
itemBuilder: (context, value) {
return ListTile(title: Text(value));
},
onSelected: (value) {
ctrl.text = value;
setState(() {
values[formItem.key] = value;
someValueChanged();
});
},
decoration: InputDecoration(
helperText: formItem.label + (formItem.required ? ' *' : ''),
hintText: formItem.hint),
minLines: formItem.max <= 1 ? null : formItem.max,
maxLines: formItem.max <= 1 ? 1 : formItem.max,
validator: (value) {
if (formItem.required &&
(value == null || value.trim().isEmpty)) {
return '${formItem.label} ${tr('requiredInBrackets')}';
}
for (var validator in formItem.additionalValidators) {
String? result = validator(value);
if (result != null) {
return result;
}
}
return null;
suggestionsCallback: (search) {
return formItem.autoCompleteOptions
?.where((t) => t.toLowerCase().contains(search.toLowerCase()))
.toList();
},
hideOnEmpty: true,
);
} else if (formItem is GeneratedFormDropdown) {
if (formItem.opts!.isEmpty) {
Expand Down
82 changes: 66 additions & 16 deletions lib/pages/add_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ class AddAppPageState extends State<AddAppPage> {
}

changeUserInput(String input, bool valid, bool isBuilding,
{bool updateUrlInput = false}) {
{bool updateUrlInput = false, String? overrideSource}) {
userInput = input;
if (!isBuilding) {
setState(() {
if (overrideSource != null) {
pickedSourceOverride = overrideSource;
}
if (updateUrlInput) {
urlInputKey++;
}
Expand All @@ -68,6 +71,7 @@ class AddAppPageState extends State<AddAppPage> {
if (pickedSource.runtimeType != source.runtimeType ||
(prevHost != null && prevHost != source?.hosts[0])) {
pickedSource = source;
pickedSource?.runOnAddAppInputChange(userInput);
additionalSettings = source != null
? getDefaultValuesFromFormItems(
source.combinedAppSpecificSettingFormItems)
Expand Down Expand Up @@ -259,9 +263,7 @@ class AddAppPageState extends State<AddAppPage> {
searching = true;
});
var sourceStrings = <String, List<String>>{};
sourceProvider.sources
.where((e) => e.canSearch && !e.excludeFromMassSearch)
.forEach((s) {
sourceProvider.sources.where((e) => e.canSearch).forEach((s) {
sourceStrings[s.name] = [s.name];
});
try {
Expand All @@ -282,32 +284,78 @@ class AddAppPageState extends State<AddAppPage> {
settingsProvider.searchDeselected = sourceStrings.keys
.where((s) => !searchSources.contains(s))
.toList();
var results = await Future.wait(sourceProvider.sources
.where((e) => searchSources.contains(e.name))
.map((e) async {
List<MapEntry<String, Map<String, List<String>>>?> results =
(await Future.wait(sourceProvider.sources
.where((e) => searchSources.contains(e.name))
.map((e) async {
try {
return await e.search(searchQuery);
Map<String, dynamic>? querySettings = {};
if (e.includeAdditionalOptsInMainSearch) {
querySettings = await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('searchX', args: [e.name]),
items: [
...e.searchQuerySettingFormItems.map((e) => [e]),
[
GeneratedFormTextField('url',
label: e.hosts.isNotEmpty
? tr('overrideSource')
: plural('url', 1).substring(2),
autoCompleteOptions: [
...(e.hosts.isNotEmpty ? [e.hosts[0]] : []),
...appsProvider.apps.values
.where((a) =>
sourceProvider
.getSource(a.app.url,
overrideSource:
a.app.overrideSource)
.runtimeType ==
e.runtimeType)
.map((a) {
var uri = Uri.parse(a.app.url);
return '${uri.origin}${uri.path}';
})
],
defaultValue:
e.hosts.isNotEmpty ? e.hosts[0] : '',
required: true)
],
],
);
});
if (querySettings == null) {
return null;
}
}
return MapEntry(e.runtimeType.toString(),
await e.search(searchQuery, querySettings: querySettings));
} catch (err) {
if (err is! CredsNeededError) {
rethrow;
} else {
err.unexpected = true;
showError(err, context);
return <String, List<String>>{};
return null;
}
}
}));
})))
.where((a) => a != null)
.toList();

// Interleave results instead of simple reduce
Map<String, List<String>> res = {};
Map<String, MapEntry<String, List<String>>> res = {};
var si = 0;
var done = false;
while (!done) {
done = true;
for (var r in results) {
if (r.length > si) {
var sourceName = r!.key;
if (r.value.length > si) {
done = false;
res.addEntries([r.entries.elementAt(si)]);
var singleRes = r.value.entries.elementAt(si);
res[singleRes.key] = MapEntry(sourceName, singleRes.value);
}
}
si++;
Expand All @@ -322,13 +370,15 @@ class AddAppPageState extends State<AddAppPage> {
context: context,
builder: (BuildContext ctx) {
return SelectionModal(
entries: res,
entries: res.map((k, v) => MapEntry(k, v.value)),
selectedByDefault: false,
onlyOneSelectionAllowed: true,
);
});
if (selectedUrls != null && selectedUrls.isNotEmpty) {
changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
var sourceName = res[selectedUrls[0]]?.key;
changeUserInput(selectedUrls[0], true, false,
updateUrlInput: true, overrideSource: sourceName);
}
}
} catch (e) {
Expand All @@ -349,7 +399,7 @@ class AddAppPageState extends State<AddAppPage> {
[
GeneratedFormDropdown(
'overrideSource',
defaultValue: '',
defaultValue: pickedSourceOverride ?? '',
[
MapEntry('', tr('none')),
...sourceProvider.sources.map(
Expand Down
Loading

0 comments on commit cd153e7

Please sign in to comment.