Added category multi-select to Apps filter

+ UI tweaks and bugfixes
This commit is contained in:
Imran Remtulla
2022-12-25 21:41:51 -05:00
parent 7af0a8628c
commit 3d6c9bbf98
6 changed files with 294 additions and 258 deletions

View File

@@ -91,6 +91,7 @@ class GeneratedFormTagInput extends GeneratedFormItem {
late bool singleSelect;
late WrapAlignment alignment;
late String emptyMessage;
late bool showLabelWhenNotEmpty;
GeneratedFormTagInput(String key,
{String label = 'Input',
List<Widget> belowWidgets = const [],
@@ -100,7 +101,8 @@ class GeneratedFormTagInput extends GeneratedFormItem {
this.deleteConfirmationMessage,
this.singleSelect = false,
this.alignment = WrapAlignment.start,
this.emptyMessage = 'Input'})
this.emptyMessage = 'Input',
this.showLabelWhenNotEmpty = true})
: super(key,
label: label,
belowWidgets: belowWidgets,
@@ -140,11 +142,11 @@ class _GeneratedFormState extends State<GeneratedForm> {
for (int r = 0; r < widget.items.length; r++) {
for (int i = 0; i < widget.items[r].length; i++) {
if (formInputs[r][i] is TextFormField) {
valid = valid &&
((formInputs[r][i].key as GlobalKey<FormFieldState>)
.currentState
?.isValid ??
false);
var fieldState =
(formInputs[r][i].key as GlobalKey<FormFieldState>).currentState;
if (fieldState != null) {
valid = valid && fieldState.isValid;
}
}
}
}
@@ -259,8 +261,30 @@ class _GeneratedFormState extends State<GeneratedForm> {
],
);
} else if (widget.items[r][e] is GeneratedFormTagInput) {
formInputs[r][e] = Wrap(
alignment: (widget.items[r][e] as GeneratedFormTagInput).alignment,
formInputs[r][e] =
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
if ((values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
?.isNotEmpty ==
true &&
(widget.items[r][e] as GeneratedFormTagInput)
.showLabelWhenNotEmpty)
Column(
crossAxisAlignment:
(widget.items[r][e] as GeneratedFormTagInput).alignment ==
WrapAlignment.center
? CrossAxisAlignment.center
: CrossAxisAlignment.stretch,
children: [
Text(widget.items[r][e].label),
const SizedBox(
height: 8,
),
],
),
Wrap(
alignment:
(widget.items[r][e] as GeneratedFormTagInput).alignment,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
(values[widget.items[r][e].key]
@@ -270,7 +294,6 @@ class _GeneratedFormState extends State<GeneratedForm> {
? Text(
(widget.items[r][e] as GeneratedFormTagInput)
.emptyMessage,
style: const TextStyle(fontWeight: FontWeight.bold),
)
: const SizedBox.shrink(),
...(values[widget.items[r][e].key]
@@ -295,20 +318,24 @@ class _GeneratedFormState extends State<GeneratedForm> {
MapEntry<int, bool>>)[e2.key]!
.key,
value);
if ((widget.items[r][e] as GeneratedFormTagInput)
if ((widget.items[r][e]
as GeneratedFormTagInput)
.singleSelect &&
value == true) {
for (var key in (values[widget.items[r][e].key]
for (var key in (values[
widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>)
.keys) {
if (key != e2.key) {
(values[widget.items[r][e].key] as Map<
String,
MapEntry<int,
bool>>)[key] = MapEntry(
(values[widget.items[r][e].key] as Map<
MapEntry<int, bool>>)[key] =
MapEntry(
(values[widget.items[r][e].key]
as Map<
String,
MapEntry<int, bool>>)[key]!
MapEntry<int,
bool>>)[key]!
.key,
false);
}
@@ -390,8 +417,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
as Map<String, MapEntry<int, bool>>?;
temp ??= {};
if (temp[label] == null) {
var singleSelect =
(widget.items[r][e] as GeneratedFormTagInput)
var singleSelect = (widget.items[r][e]
as GeneratedFormTagInput)
.singleSelect;
var someSelected = temp.entries
.where((element) => element.value.value)
@@ -411,7 +438,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
tooltip: tr('add'),
)),
],
);
)
]);
}
}
}

View File

@@ -9,12 +9,14 @@ class GeneratedFormModal extends StatefulWidget {
required this.title,
required this.items,
this.initValid = false,
this.message = ''});
this.message = '',
this.additionalWidgets = const []});
final String title;
final String message;
final List<List<GeneratedFormItem>> items;
final bool initValid;
final List<Widget> additionalWidgets;
@override
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
@@ -54,7 +56,8 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
this.valid = valid;
});
}
})
}),
if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets
]),
actions: [
TextButton(

View File

@@ -39,7 +39,8 @@ class _AddAppPageState extends State<AddAppPage> {
changeUserInput(String input, bool valid, bool isBuilding) {
userInput = input;
fn() {
if (!isBuilding) {
setState(() {
var source = valid ? sourceProvider.getSource(userInput) : null;
if (pickedSource.runtimeType != source.runtimeType) {
pickedSource = source;
@@ -51,13 +52,6 @@ class _AddAppPageState extends State<AddAppPage> {
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
: true;
}
}
if (isBuilding) {
fn();
} else {
setState(() {
fn();
});
}
}
@@ -243,7 +237,9 @@ class _AddAppPageState extends State<AddAppPage> {
]
],
onValueChanges: (values, valid, isBuilding) {
if (values.isNotEmpty && valid) {
if (values.isNotEmpty &&
valid &&
!isBuilding) {
setState(() {
searchQuery =
values['searchSomeSources']!.trim();

View File

@@ -75,7 +75,7 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 100),
const SizedBox(height: 150),
app?.installedInfo != null
? Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -168,7 +168,7 @@ class _AppPageState extends State<AppPage> {
appsProvider.saveApps([app.app]);
}
}),
const SizedBox(height: 100)
const SizedBox(height: 150)
],
)),
],

View File

@@ -7,6 +7,7 @@ import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/pages/app.dart';
import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
@@ -22,7 +23,8 @@ class AppsPage extends StatefulWidget {
}
class AppsPageState extends State<AppsPage> {
AppsFilter? filter;
AppsFilter filter = AppsFilter();
final AppsFilter neutralFilter = AppsFilter();
var updatesOnlyFilter =
AppsFilter(includeUptodate: false, includeNonInstalled: false);
Set<App> selectedApps = {};
@@ -53,8 +55,7 @@ class AppsPageState extends State<AppsPage> {
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
var sortedApps = appsProvider.apps.values.toList();
var currentFilterIsUpdatesOnly =
filter?.isIdenticalTo(updatesOnlyFilter) ?? false;
var currentFilterIsUpdatesOnly = filter.isIdenticalTo(updatesOnlyFilter);
selectedApps = selectedApps
.where((element) => sortedApps.map((e) => e.app).contains(element))
@@ -70,22 +71,20 @@ class AppsPageState extends State<AppsPage> {
});
}
if (filter != null) {
sortedApps = sortedApps.where((app) {
if (app.app.installedVersion == app.app.latestVersion &&
!(filter!.includeUptodate)) {
!(filter.includeUptodate)) {
return false;
}
if (app.app.installedVersion == null &&
!(filter!.includeNonInstalled)) {
if (app.app.installedVersion == null && !(filter.includeNonInstalled)) {
return false;
}
if (filter!.nameFilter.isNotEmpty || filter!.authorFilter.isNotEmpty) {
List<String> nameTokens = filter!.nameFilter
if (filter.nameFilter.isNotEmpty || filter.authorFilter.isNotEmpty) {
List<String> nameTokens = filter.nameFilter
.split(' ')
.where((element) => element.trim().isNotEmpty)
.toList();
List<String> authorTokens = filter!.authorFilter
List<String> authorTokens = filter.authorFilter
.split(' ')
.where((element) => element.trim().isNotEmpty)
.toList();
@@ -102,13 +101,12 @@ class AppsPageState extends State<AppsPage> {
}
}
}
if (filter!.categoryFilter.isNotEmpty &&
filter!.categoryFilter != app.app.category) {
if (filter.categoryFilter.isNotEmpty &&
!filter.categoryFilter.contains(app.app.category)) {
return false;
}
return true;
}).toList();
}
sortedApps.sort((a, b) {
var nameA = a.installedInfo?.name ?? a.app.name;
@@ -663,7 +661,7 @@ class AppsPageState extends State<AppsPage> {
onPressed: () {
setState(() {
if (currentFilterIsUpdatesOnly) {
filter = null;
filter = AppsFilter();
} else {
filter = updatesOnlyFilter;
}
@@ -683,9 +681,11 @@ class AppsPageState extends State<AppsPage> {
? const SizedBox()
: TextButton.icon(
label: Text(
filter == null ? tr('filter') : tr('filterActive'),
filter.isIdenticalTo(neutralFilter)
? tr('filter')
: tr('filterActive'),
style: TextStyle(
fontWeight: filter == null
fontWeight: filter.isIdenticalTo(neutralFilter)
? FontWeight.normal
: FontWeight.bold),
),
@@ -693,10 +693,9 @@ class AppsPageState extends State<AppsPage> {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var vals = filter == null
? AppsFilter().toValuesMap()
: filter!.toValuesMap();
var vals = filter.toFormValuesMap();
return GeneratedFormModal(
initValid: true,
title: tr('filterApps'),
items: [
[
@@ -718,19 +717,24 @@ class AppsPageState extends State<AppsPage> {
GeneratedFormSwitch('nonInstalledApps',
label: tr('nonInstalledApps'),
defaultValue: vals['nonInstalledApps'])
],
[
settingsProvider.getCategoryFormItem(
initCategory: vals['category'] ?? '')
]
]);
],
additionalWidgets: [
const SizedBox(
height: 16,
),
CategoryEditorSelector(
preselected: filter.categoryFilter,
onSelected: (categories) {
filter.categoryFilter = categories.toSet();
},
)
],
);
}).then((values) {
if (values != null) {
setState(() {
filter = AppsFilter.fromValuesMap(values);
if (AppsFilter().isIdenticalTo(filter!)) {
filter = null;
}
filter.setFormValuesFromMap(values);
});
}
});
@@ -748,31 +752,29 @@ class AppsFilter {
late String authorFilter;
late bool includeUptodate;
late bool includeNonInstalled;
late String categoryFilter;
late Set<String> categoryFilter;
AppsFilter(
{this.nameFilter = '',
this.authorFilter = '',
this.includeUptodate = true,
this.includeNonInstalled = true,
this.categoryFilter = ''});
this.categoryFilter = const {}});
Map<String, dynamic> toValuesMap() {
Map<String, dynamic> toFormValuesMap() {
return {
'appName': nameFilter,
'author': authorFilter,
'upToDateApps': includeUptodate,
'nonInstalledApps': includeNonInstalled,
'category': categoryFilter
'nonInstalledApps': includeNonInstalled
};
}
AppsFilter.fromValuesMap(Map<String, dynamic> values) {
setFormValuesFromMap(Map<String, dynamic> values) {
nameFilter = values['appName']!;
authorFilter = values['author']!;
includeUptodate = values['upToDateApps'];
includeNonInstalled = values['nonInstalledApps'];
categoryFilter = values['category']!;
}
bool isIdenticalTo(AppsFilter other) =>
@@ -780,5 +782,7 @@ class AppsFilter {
nameFilter.trim() == other.nameFilter.trim() &&
includeUptodate == other.includeUptodate &&
includeNonInstalled == other.includeNonInstalled &&
categoryFilter.trim() == other.categoryFilter.trim();
categoryFilter.length == other.categoryFilter.length &&
categoryFilter.union(other.categoryFilter).length ==
categoryFilter.length;
}

View File

@@ -185,7 +185,7 @@ class _SettingsPageState extends State<SettingsPage> {
return [e];
}).toList(),
onValueChanges: (values, valid, isBuilding) {
if (valid) {
if (valid && !isBuilding) {
values.forEach((key, value) {
settingsProvider.setSettingString(key, value);
});
@@ -286,7 +286,9 @@ class _SettingsPageState extends State<SettingsPage> {
color: Theme.of(context).colorScheme.primary),
),
height16,
const CategoryEditorSelector()
const CategoryEditorSelector(
showLabelWhenNotEmpty: false,
)
],
))),
SliverToBoxAdapter(
@@ -407,12 +409,14 @@ class CategoryEditorSelector extends StatefulWidget {
final bool singleSelect;
final Set<String> preselected;
final WrapAlignment alignment;
final bool showLabelWhenNotEmpty;
const CategoryEditorSelector(
{super.key,
this.onSelected,
this.singleSelect = false,
this.preselected = const {},
this.alignment = WrapAlignment.start});
this.alignment = WrapAlignment.start,
this.showLabelWhenNotEmpty = true});
@override
State<CategoryEditorSelector> createState() => _CategoryEditorSelectorState();
@@ -439,7 +443,8 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
deleteConfirmationMessage: MapEntry(
tr('deleteCategoriesQuestion'),
tr('categoryDeleteWarning')),
singleSelect: widget.singleSelect)
singleSelect: widget.singleSelect,
showLabelWhenNotEmpty: widget.showLabelWhenNotEmpty)
]
],
onValueChanges: ((values, valid, isBuilding) {