mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-17 20:28:10 +02:00
Added search bar on Add App page
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:obtainium/components/generated_form.dart';
|
|||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/pages/app.dart';
|
import 'package:obtainium/pages/app.dart';
|
||||||
|
import 'package:obtainium/pages/import_export.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@@ -22,6 +23,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
bool gettingAppInfo = false;
|
bool gettingAppInfo = false;
|
||||||
|
|
||||||
String userInput = '';
|
String userInput = '';
|
||||||
|
String searchQuery = '';
|
||||||
AppSource? pickedSource;
|
AppSource? pickedSource;
|
||||||
List<String> sourceSpecificAdditionalData = [];
|
List<String> sourceSpecificAdditionalData = [];
|
||||||
bool sourceSpecificDataIsValid = true;
|
bool sourceSpecificDataIsValid = true;
|
||||||
@@ -31,6 +33,107 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
|
|
||||||
|
changeUserInput(String input, bool valid, bool isBuilding) {
|
||||||
|
userInput = input;
|
||||||
|
fn() {
|
||||||
|
var source = valid ? sourceProvider.getSource(userInput) : null;
|
||||||
|
if (pickedSource != source) {
|
||||||
|
pickedSource = source;
|
||||||
|
sourceSpecificAdditionalData =
|
||||||
|
source != null ? source.additionalSourceAppSpecificDefaults : [];
|
||||||
|
sourceSpecificDataIsValid = source != null
|
||||||
|
? sourceProvider.ifSourceAppsRequireAdditionalData(source)
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBuilding) {
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addApp({bool resetUserInputAfter = false}) async {
|
||||||
|
setState(() {
|
||||||
|
gettingAppInfo = true;
|
||||||
|
});
|
||||||
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
|
() async {
|
||||||
|
var userPickedTrackOnly = findGeneratedFormValueByKey(
|
||||||
|
pickedSource!.additionalAppSpecificSourceAgnosticFormItems,
|
||||||
|
otherAdditionalData,
|
||||||
|
'trackOnlyFormItemKey') ==
|
||||||
|
'true';
|
||||||
|
var cont = true;
|
||||||
|
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title:
|
||||||
|
'${pickedSource!.enforceTrackOnly ? 'Source' : 'App'} is Track-Only',
|
||||||
|
items: const [],
|
||||||
|
defaultValues: const [],
|
||||||
|
message:
|
||||||
|
'${pickedSource!.enforceTrackOnly ? 'Apps from this source are \'Track-Only\'.' : 'You have selected the \'Track-Only\' option.'}\n\nThe App will be tracked for updates, but Obtainium will not be able to download or install it.',
|
||||||
|
);
|
||||||
|
}) ==
|
||||||
|
null) {
|
||||||
|
cont = false;
|
||||||
|
}
|
||||||
|
if (cont) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||||
|
App app = await sourceProvider.getApp(
|
||||||
|
pickedSource!, userInput, sourceSpecificAdditionalData,
|
||||||
|
trackOnly: trackOnly);
|
||||||
|
if (!trackOnly) {
|
||||||
|
await settingsProvider.getInstallPermission();
|
||||||
|
}
|
||||||
|
// Only download the APK here if you need to for the package ID
|
||||||
|
if (sourceProvider.isTempId(app.id) && !app.trackOnly) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (appsProvider.apps.containsKey(app.id)) {
|
||||||
|
throw ObtainiumError('App already added');
|
||||||
|
}
|
||||||
|
if (app.trackOnly) {
|
||||||
|
app.installedVersion = app.latestVersion;
|
||||||
|
}
|
||||||
|
await appsProvider.saveApps([app]);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
.then((app) {
|
||||||
|
if (app != null) {
|
||||||
|
Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (context) => AppPage(appId: app.id)));
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
gettingAppInfo = false;
|
||||||
|
if (resetUserInputAfter) {
|
||||||
|
changeUserInput('', false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@@ -70,34 +173,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
fn() {
|
changeUserInput(
|
||||||
userInput = values[0];
|
values[0], valid, isBuilding);
|
||||||
var source = valid
|
|
||||||
? sourceProvider.getSource(userInput)
|
|
||||||
: null;
|
|
||||||
if (pickedSource != source) {
|
|
||||||
pickedSource = source;
|
|
||||||
sourceSpecificAdditionalData = source !=
|
|
||||||
null
|
|
||||||
? source
|
|
||||||
.additionalSourceAppSpecificDefaults
|
|
||||||
: [];
|
|
||||||
sourceSpecificDataIsValid = source !=
|
|
||||||
null
|
|
||||||
? sourceProvider
|
|
||||||
.ifSourceAppsRequireAdditionalData(
|
|
||||||
source)
|
|
||||||
: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBuilding) {
|
|
||||||
fn();
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
fn();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
defaultValues: const [])),
|
defaultValues: const [])),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -117,111 +194,91 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
.isNotEmpty &&
|
.isNotEmpty &&
|
||||||
!otherAdditionalDataIsValid)
|
!otherAdditionalDataIsValid)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: addApp,
|
||||||
setState(() {
|
|
||||||
gettingAppInfo = true;
|
|
||||||
});
|
|
||||||
var appsProvider =
|
|
||||||
context.read<AppsProvider>();
|
|
||||||
var settingsProvider =
|
|
||||||
context.read<SettingsProvider>();
|
|
||||||
() async {
|
|
||||||
var userPickedTrackOnly =
|
|
||||||
findGeneratedFormValueByKey(
|
|
||||||
pickedSource!
|
|
||||||
.additionalAppSpecificSourceAgnosticFormItems,
|
|
||||||
otherAdditionalData,
|
|
||||||
'trackOnlyFormItemKey') ==
|
|
||||||
'true';
|
|
||||||
var cont = true;
|
|
||||||
if ((userPickedTrackOnly ||
|
|
||||||
pickedSource!
|
|
||||||
.enforceTrackOnly) &&
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title:
|
|
||||||
'${pickedSource!.enforceTrackOnly ? 'Source' : 'App'} is Track-Only',
|
|
||||||
items: const [],
|
|
||||||
defaultValues: const [],
|
|
||||||
message:
|
|
||||||
'${pickedSource!.enforceTrackOnly ? 'Apps from this source are \'Track-Only\'.' : 'You have selected the \'Track-Only\' option.'}\n\nThe App will be tracked for updates, but Obtainium will not be able to download or install it.',
|
|
||||||
);
|
|
||||||
}) ==
|
|
||||||
null) {
|
|
||||||
cont = false;
|
|
||||||
}
|
|
||||||
if (cont) {
|
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
var trackOnly = pickedSource!
|
|
||||||
.enforceTrackOnly ||
|
|
||||||
userPickedTrackOnly;
|
|
||||||
App app =
|
|
||||||
await sourceProvider.getApp(
|
|
||||||
pickedSource!,
|
|
||||||
userInput,
|
|
||||||
sourceSpecificAdditionalData,
|
|
||||||
trackOnly: trackOnly);
|
|
||||||
if (!trackOnly) {
|
|
||||||
await settingsProvider
|
|
||||||
.getInstallPermission();
|
|
||||||
}
|
|
||||||
// Only download the APK here if you need to for the package ID
|
|
||||||
if (sourceProvider
|
|
||||||
.isTempId(app.id) &&
|
|
||||||
!app.trackOnly) {
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (appsProvider.apps
|
|
||||||
.containsKey(app.id)) {
|
|
||||||
throw ObtainiumError(
|
|
||||||
'App already added');
|
|
||||||
}
|
|
||||||
if (app.trackOnly) {
|
|
||||||
app.installedVersion =
|
|
||||||
app.latestVersion;
|
|
||||||
}
|
|
||||||
await appsProvider
|
|
||||||
.saveApps([app]);
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
.then((app) {
|
|
||||||
if (app != null) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
AppPage(
|
|
||||||
appId: app.id)));
|
|
||||||
}
|
|
||||||
}).catchError((e) {
|
|
||||||
showError(e, context);
|
|
||||||
}).whenComplete(() {
|
|
||||||
setState(() {
|
|
||||||
gettingAppInfo = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Text('Add'))
|
child: const Text('Add'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (sourceProvider.sources
|
||||||
|
.where((e) => e.canSearch)
|
||||||
|
.isNotEmpty &&
|
||||||
|
pickedSource == null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GeneratedForm(
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Search (Some Sources Only)',
|
||||||
|
required: false),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
if (values.isNotEmpty && valid) {
|
||||||
|
setState(() {
|
||||||
|
searchQuery = values[0].trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultValues: const ['']),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: searchQuery.isEmpty || gettingAppInfo
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
Future.wait(sourceProvider.sources
|
||||||
|
.where((e) => e.canSearch)
|
||||||
|
.map((e) =>
|
||||||
|
e.search(searchQuery)))
|
||||||
|
.then((results) async {
|
||||||
|
var res = // TODO: Interleave results
|
||||||
|
results.reduce((value, element) {
|
||||||
|
value.addAll(element);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
// Map<String, String> res = {};
|
||||||
|
// var si = 0;
|
||||||
|
// var done = false;
|
||||||
|
// for (var r in results) {
|
||||||
|
// if (r.length > si) {
|
||||||
|
// res.addEntries(r.entries.toList()[si]);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// for (var rs in results) {
|
||||||
|
// for (var r in rs.entries) {}
|
||||||
|
// }
|
||||||
|
List<String>? selectedUrls = res
|
||||||
|
.isEmpty
|
||||||
|
? []
|
||||||
|
: await showDialog<List<String>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return UrlSelectionModal(
|
||||||
|
urlsWithDescriptions: res,
|
||||||
|
selectedByDefault: false,
|
||||||
|
onlyOneSelectionAllowed:
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (selectedUrls != null &&
|
||||||
|
selectedUrls.isNotEmpty) {
|
||||||
|
changeUserInput(
|
||||||
|
selectedUrls[0], true, true);
|
||||||
|
addApp(resetUserInputAfter: true);
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Search'))
|
||||||
|
],
|
||||||
|
),
|
||||||
if (pickedSource != null &&
|
if (pickedSource != null &&
|
||||||
(pickedSource!.additionalSourceAppSpecificDefaults
|
(pickedSource!.additionalSourceAppSpecificDefaults
|
||||||
.isNotEmpty ||
|
.isNotEmpty ||
|
||||||
@@ -314,7 +371,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
LaunchMode.externalApplication);
|
LaunchMode.externalApplication);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'${e.runtimeType.toString()}${e.enforceTrackOnly ? ' (Track-Only)' : ''}',
|
'${e.runtimeType.toString()}${e.enforceTrackOnly ? ' (Track-Only)' : ''}${e.canSearch ? ' (Searchable)' : ''}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration:
|
decoration:
|
||||||
TextDecoration.underline,
|
TextDecoration.underline,
|
||||||
@@ -322,6 +379,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
)))
|
)))
|
||||||
.toList()
|
.toList()
|
||||||
])),
|
])),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
])),
|
])),
|
||||||
)
|
)
|
||||||
]));
|
]));
|
||||||
|
@@ -38,23 +38,6 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<List<String>>> addApps(List<String> urls) async {
|
|
||||||
List<dynamic> results = await sourceProvider.getAppsByURLNaive(urls,
|
|
||||||
ignoreUrls: appsProvider.apps.values.map((e) => e.app.url).toList());
|
|
||||||
List<App> apps = results[0];
|
|
||||||
Map<String, dynamic> errorsMap = results[1];
|
|
||||||
for (var app in apps) {
|
|
||||||
if (appsProvider.apps.containsKey(app.id)) {
|
|
||||||
errorsMap.addAll({app.id: 'App already added'});
|
|
||||||
} else {
|
|
||||||
await appsProvider.saveApps([app]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<List<String>> errors =
|
|
||||||
errorsMap.keys.map((e) => [e, errorsMap[e].toString()]).toList();
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@@ -194,7 +177,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
addApps(urls).then((errors) {
|
appsProvider
|
||||||
|
.addAppsByURL(urls)
|
||||||
|
.then((errors) {
|
||||||
if (errors.isEmpty) {
|
if (errors.isEmpty) {
|
||||||
showError(
|
showError(
|
||||||
'Imported ${urls.length} Apps',
|
'Imported ${urls.length} Apps',
|
||||||
@@ -272,7 +257,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
return UrlSelectionModal(
|
return UrlSelectionModal(
|
||||||
urlsWithDescriptions:
|
urlsWithDescriptions:
|
||||||
urlsWithDescriptions,
|
urlsWithDescriptions,
|
||||||
defaultSelected:
|
selectedByDefault:
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -281,8 +266,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
selectedUrls
|
selectedUrls
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
var errors =
|
var errors =
|
||||||
await addApps(
|
await appsProvider
|
||||||
selectedUrls);
|
.addAppsByURL(
|
||||||
|
selectedUrls);
|
||||||
if (errors.isEmpty) {
|
if (errors.isEmpty) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showError(
|
showError(
|
||||||
@@ -371,8 +357,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
});
|
});
|
||||||
if (selectedUrls != null) {
|
if (selectedUrls != null) {
|
||||||
var errors =
|
var errors =
|
||||||
await addApps(
|
await appsProvider
|
||||||
selectedUrls);
|
.addAppsByURL(
|
||||||
|
selectedUrls);
|
||||||
if (errors.isEmpty) {
|
if (errors.isEmpty) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showError(
|
showError(
|
||||||
@@ -483,10 +470,12 @@ class UrlSelectionModal extends StatefulWidget {
|
|||||||
UrlSelectionModal(
|
UrlSelectionModal(
|
||||||
{super.key,
|
{super.key,
|
||||||
required this.urlsWithDescriptions,
|
required this.urlsWithDescriptions,
|
||||||
this.defaultSelected = true});
|
this.selectedByDefault = true,
|
||||||
|
this.onlyOneSelectionAllowed = false});
|
||||||
|
|
||||||
Map<String, String> urlsWithDescriptions;
|
Map<String, String> urlsWithDescriptions;
|
||||||
bool defaultSelected;
|
bool selectedByDefault;
|
||||||
|
bool onlyOneSelectionAllowed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UrlSelectionModal> createState() => _UrlSelectionModalState();
|
State<UrlSelectionModal> createState() => _UrlSelectionModalState();
|
||||||
@@ -498,8 +487,17 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
for (var url in widget.urlsWithDescriptions.entries) {
|
for (var url in widget.urlsWithDescriptions.entries) {
|
||||||
urlWithDescriptionSelections.putIfAbsent(
|
urlWithDescriptionSelections.putIfAbsent(url,
|
||||||
url, () => widget.defaultSelected);
|
() => widget.selectedByDefault && !widget.onlyOneSelectionAllowed);
|
||||||
|
}
|
||||||
|
if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
|
||||||
|
selectOnlyOne(widget.urlsWithDescriptions.entries.first.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOnlyOne(String url) {
|
||||||
|
for (var uwd in urlWithDescriptionSelections.keys) {
|
||||||
|
urlWithDescriptionSelections[uwd] = uwd.key == url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +505,8 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: const Text('Select URLs to Import'),
|
title:
|
||||||
|
Text(widget.onlyOneSelectionAllowed ? 'Select URL' : 'Select URLs'),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||||
return Row(children: [
|
return Row(children: [
|
||||||
@@ -515,7 +514,12 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
value: urlWithDescriptionSelections[urlWithD],
|
value: urlWithDescriptionSelections[urlWithD],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
urlWithDescriptionSelections[urlWithD] = value ?? false;
|
value ??= false;
|
||||||
|
if (value! && widget.onlyOneSelectionAllowed) {
|
||||||
|
selectOnlyOne(urlWithD.key);
|
||||||
|
} else {
|
||||||
|
urlWithDescriptionSelections[urlWithD] = value!;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -562,14 +566,19 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
},
|
},
|
||||||
child: const Text('Cancel')),
|
child: const Text('Cancel')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed:
|
||||||
Navigator.of(context).pop(urlWithDescriptionSelections.entries
|
urlWithDescriptionSelections.values.where((b) => b).isEmpty
|
||||||
.where((entry) => entry.value)
|
? null
|
||||||
.map((e) => e.key.key)
|
: () {
|
||||||
.toList());
|
Navigator.of(context).pop(urlWithDescriptionSelections
|
||||||
},
|
.entries
|
||||||
child: Text(
|
.where((entry) => entry.value)
|
||||||
'Import ${urlWithDescriptionSelections.values.where((b) => b).length} URLs'))
|
.map((e) => e.key.key)
|
||||||
|
.toList());
|
||||||
|
},
|
||||||
|
child: Text(widget.onlyOneSelectionAllowed
|
||||||
|
? 'Pick'
|
||||||
|
: 'Import ${urlWithDescriptionSelections.values.where((b) => b).length} URLs'))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -635,6 +635,23 @@ class AppsProvider with ChangeNotifier {
|
|||||||
foregroundSubscription?.cancel();
|
foregroundSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<List<String>>> addAppsByURL(List<String> urls) async {
|
||||||
|
List<dynamic> results = await SourceProvider().getAppsByURLNaive(urls,
|
||||||
|
ignoreUrls: apps.values.map((e) => e.app.url).toList());
|
||||||
|
List<App> pps = results[0];
|
||||||
|
Map<String, dynamic> errorsMap = results[1];
|
||||||
|
for (var app in pps) {
|
||||||
|
if (apps.containsKey(app.id)) {
|
||||||
|
errorsMap.addAll({app.id: 'App already added'});
|
||||||
|
} else {
|
||||||
|
await saveApps([app]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<List<String>> errors =
|
||||||
|
errorsMap.keys.map((e) => [e, errorsMap[e].toString()]).toList();
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class APKPicker extends StatefulWidget {
|
class APKPicker extends StatefulWidget {
|
||||||
|
Reference in New Issue
Block a user