mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-23 17:39:42 +02:00
Added category multi-select to Apps filter
+ UI tweaks and bugfixes
This commit is contained in:
@@ -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'),
|
||||
)),
|
||||
],
|
||||
);
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -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();
|
||||
|
@@ -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)
|
||||
],
|
||||
)),
|
||||
],
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user