feat: Add hive type adapters to api models, migrate to freezed

This commit is contained in:
Anton Stubenbord
2023-04-24 01:14:20 +02:00
parent 5c0ef7f853
commit 1f335119b3
67 changed files with 2075 additions and 1079 deletions

View File

@@ -44,12 +44,12 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
_options = widget.options.values.toList();
final value = widget.initialValue;
if (value is IdsTagsQuery) {
_include = value.includedIds.toList();
_exclude = value.excludedIds.toList();
_include = value.include.toList();
_exclude = value.include.toList();
} else if (value is AnyAssignedTagsQuery) {
_include = value.tagIds.toList();
_anyAssigned = true;
} else if (value is OnlyNotAssignedTagsQuery) {
} else if (value is NotAssignedTagsQuery) {
_notAssigned = true;
}
_textEditingController.addListener(() => setState(() {
@@ -113,28 +113,24 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
icon: const Icon(Icons.done),
onPressed: () {
if (widget.allowOnlySelection) {
widget.onSubmit(returnValue: IdsTagsQuery.included(_include));
widget.onSubmit(returnValue: TagsQuery.ids(include: _include));
return;
}
late final TagsQuery query;
if (_notAssigned) {
query = const OnlyNotAssignedTagsQuery();
query = const TagsQuery.notAssigned();
} else if (_anyAssigned) {
query = AnyAssignedTagsQuery(tagIds: _include);
query = TagsQuery.anyAssigned(tagIds: _include);
} else {
query = IdsTagsQuery([
for (var id in _include) IncludeTagIdQuery(id),
for (var id in _exclude) ExcludeTagIdQuery(id),
]);
query = TagsQuery.ids(include: _include, exclude: _exclude);
}
widget.onSubmit(returnValue: query);
},
),
],
bottom: PreferredSize(
preferredSize: !widget.allowOnlySelection
? const Size.fromHeight(32)
: const Size.fromHeight(1),
preferredSize:
!widget.allowOnlySelection ? const Size.fromHeight(32) : const Size.fromHeight(1),
child: Column(
children: [
Divider(color: theme.colorScheme.outline),
@@ -237,8 +233,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
yield _buildNotAssignedOption();
}
var matches = _options
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
var matches = _options.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
if (matches.isEmpty && widget.allowCreation) {
yield Text(S.of(context)!.noItemsFound);
yield TextButton(
@@ -304,9 +299,7 @@ class SelectableTagWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
title: Text(tag.name),
trailing: excluded
? const Icon(Icons.close)
: (selected ? const Icon(Icons.done) : null),
trailing: excluded ? const Icon(Icons.close) : (selected ? const Icon(Icons.done) : null),
leading: CircleAvatar(
backgroundColor: tag.color,
child: (tag.isInboxTag)

View File

@@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:animations/animations.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
@@ -32,9 +33,9 @@ class TagsFormField extends StatelessWidget {
initialValue: initialValue,
builder: (field) {
final values = _generateOptions(context, field.value, field).toList();
final isEmpty = (field.value is IdsTagsQuery &&
(field.value as IdsTagsQuery).ids.isEmpty) ||
field.value == null;
final isEmpty =
(field.value is IdsTagsQuery && (field.value as IdsTagsQuery).include.isEmpty) ||
field.value == null;
bool anyAssigned = field.value is AnyAssignedTagsQuery;
return OpenContainer<TagsQuery>(
middleColor: Theme.of(context).colorScheme.background,
@@ -59,8 +60,7 @@ class TagsFormField extends StatelessWidget {
height: 32,
child: ListView.separated(
scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) =>
const SizedBox(width: 4),
separatorBuilder: (context, index) => const SizedBox(width: 4),
itemBuilder: (context, index) => values[index],
itemCount: values.length,
),
@@ -93,33 +93,56 @@ class TagsFormField extends StatelessWidget {
) sync* {
if (query == null) {
yield Container();
} else if (query is IdsTagsQuery) {
for (final e in query.queries) {
yield _buildTagIdQueryWidget(context, e, field);
}
} else if (query is OnlyNotAssignedTagsQuery) {
yield _buildNotAssignedTagWidget(context, field);
} else if (query is AnyAssignedTagsQuery) {
for (final e in query.tagIds) {
yield _buildAnyAssignedTagWidget(context, e, field, query);
} else {
final widgets = query.map(
ids: (value) => [
for (var inc in value.include) _buildTagIdQueryWidget(context, inc, field, false),
for (var exc in value.exclude) _buildTagIdQueryWidget(context, exc, field, true),
],
anyAssigned: (value) => [
for (var id in value.tagIds) _buildAnyAssignedTagWidget(context, id, field, value),
],
notAssigned: (value) => [_buildNotAssignedTagWidget(context, field)],
);
for (var child in widgets) {
yield child;
}
}
}
Widget _buildTagIdQueryWidget(
BuildContext context,
TagIdQuery e,
int id,
FormFieldState<TagsQuery?> field,
bool exclude,
) {
assert(field.value is IdsTagsQuery);
final formValue = field.value as IdsTagsQuery;
final tag = options[e.id]!;
final tag = options[id]!;
return QueryTagChip(
onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])),
onDeleted: () => field.didChange(formValue.copyWith(
include: formValue.include.whereNot((element) => element == id),
exclude: formValue.exclude.whereNot((element) => element == id),
)),
onSelected: allowExclude
? () => field.didChange(formValue.withIdQueryToggled(e.id))
? () {
if (formValue.include.contains(id)) {
field.didChange(
formValue.copyWith(
include: formValue.include.whereNot((element) => element == id),
exclude: [...formValue.exclude, id],
),
);
} else if (formValue.exclude.contains(id)) {}
field.didChange(
formValue.copyWith(
include: [...formValue.include, id],
exclude: formValue.exclude.whereNot((element) => element == id),
),
);
}
: null,
exclude: e is ExcludeTagIdQuery,
exclude: exclude,
backgroundColor: tag.color,
foregroundColor: tag.textColor,
labelText: tag.name,
@@ -147,9 +170,11 @@ class TagsFormField extends StatelessWidget {
) {
return QueryTagChip(
onDeleted: () {
final updatedQuery = query.withRemoved([e]);
final updatedQuery = query.copyWith(
tagIds: query.tagIds.whereNot((element) => element == e),
);
if (updatedQuery.tagIds.isEmpty) {
field.didChange(const IdsTagsQuery());
field.didChange(const TagsQuery.ids());
} else {
field.didChange(updatedQuery);
}

View File

@@ -27,12 +27,9 @@ class LabelsPage extends StatefulWidget {
State<LabelsPage> createState() => _LabelsPageState();
}
class _LabelsPageState extends State<LabelsPage>
with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle tabBarHandle =
SliverOverlapAbsorberHandle();
class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle();
late final TabController _tabController;
int _currentIndex = 0;
@@ -82,33 +79,25 @@ class _LabelsPageState extends State<LabelsPage>
Tab(
icon: Icon(
Icons.person_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.description_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.label_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
Tab(
icon: Icon(
Icons.folder_open,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
@@ -126,20 +115,17 @@ class _LabelsPageState extends State<LabelsPage>
return true;
}
final desiredTab =
((metrics.pixels / metrics.maxScrollExtent) *
(_tabController.length - 1))
((metrics.pixels / metrics.maxScrollExtent) * (_tabController.length - 1))
.round();
if (metrics.axis == Axis.horizontal &&
_currentIndex != desiredTab) {
if (metrics.axis == Axis.horizontal && _currentIndex != desiredTab) {
setState(() => _currentIndex = desiredTab);
}
return true;
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
notificationPredicate: (notification) => connectedState.isConnected,
onRefresh: () => [
context.read<LabelCubit>().reloadCorrespondents,
context.read<LabelCubit>().reloadDocumentTypes,
@@ -157,20 +143,14 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: context
.watch<LabelCubit>()
.state
.correspondents,
labels: context.watch<LabelCubit>().state.correspondents,
filterBuilder: (label) => DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id),
correspondent: IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel:
S.of(context)!.addNewCorrespondent,
emptyStateDescription:
S.of(context)!.noCorrespondentsSetUp,
emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent,
emptyStateDescription: S.of(context)!.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
@@ -184,20 +164,14 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: context
.watch<LabelCubit>()
.state
.documentTypes,
labels: context.watch<LabelCubit>().state.documentTypes,
filterBuilder: (label) => DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id),
documentType: IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel:
S.of(context)!.addNewDocumentType,
emptyStateDescription:
S.of(context)!.noDocumentTypesSetUp,
emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType,
emptyStateDescription: S.of(context)!.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
@@ -211,10 +185,9 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Tag>(
labels:
context.watch<LabelCubit>().state.tags,
labels: context.watch<LabelCubit>().state.tags,
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
tags: TagsQuery.ids(include: [label.id!]),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditTagPage,
@@ -227,10 +200,8 @@ class _LabelsPageState extends State<LabelsPage>
)
: null,
),
emptyStateActionButtonLabel:
S.of(context)!.addNewTag,
emptyStateDescription:
S.of(context)!.noTagsSetUp,
emptyStateActionButtonLabel: S.of(context)!.addNewTag,
emptyStateDescription: S.of(context)!.noTagsSetUp,
onAddNew: _openAddTagPage,
),
],
@@ -244,21 +215,15 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: context
.watch<LabelCubit>()
.state
.storagePaths,
labels: context.watch<LabelCubit>().state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath:
IdQueryParameter.fromId(label.id),
storagePath: IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
contentBuilder: (path) => Text(path.path),
emptyStateActionButtonLabel:
S.of(context)!.addNewStoragePath,
emptyStateDescription:
S.of(context)!.noStoragePathsSetUp,
emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath,
emptyStateDescription: S.of(context)!.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],

View File

@@ -28,10 +28,10 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
this.addNewLabelText,
this.autofocus = true,
}) : assert(
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
!(initialValue?.isOnlyAssigned() ?? false) || showAnyAssignedOption,
),
assert(
!(initialValue?.onlyNotAssigned ?? false) || showNotAssignedOption,
!(initialValue?.isOnlyNotAssigned() ?? false) || showNotAssignedOption,
),
assert((addNewLabelText != null) == (onCreateNewLabel != null));
@@ -39,8 +39,7 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState();
}
class _FullscreenLabelFormState<T extends Label>
extends State<FullscreenLabelForm<T>> {
class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelForm<T>> {
bool _showClearIcon = false;
final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
@@ -80,7 +79,12 @@ class _FullscreenLabelFormState<T extends Label>
FocusScope.of(context).unfocus();
final index = AutocompleteHighlightedOption.of(context);
final value = index.isNegative ? null : options.elementAt(index);
widget.onSubmit(returnValue: IdQueryParameter.fromId(value?.id));
widget.onSubmit(
returnValue: IdQueryParameter.fromId(
value?.whenOrNull(
fromId: (id) => id,
),
));
},
autofocus: true,
style: theme.textTheme.bodyLarge?.apply(
@@ -124,11 +128,9 @@ class _FullscreenLabelFormState<T extends Label>
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
final highlight =
AutocompleteHighlightedOption.of(context) == index;
final highlight = AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0,
@@ -183,7 +185,8 @@ class _FullscreenLabelFormState<T extends Label>
}
for (final option in widget.options.values) {
// Don't include the initial value in the selection
if (option.id == widget.initialValue?.id) {
final initialValue = widget.initialValue;
if (initialValue is SetIdQueryParameter && option.id == initialValue.id) {
continue;
}
yield IdQueryParameter.fromId(option.id);
@@ -191,8 +194,8 @@ class _FullscreenLabelFormState<T extends Label>
}
} else {
// Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed.
final matches = widget.options.values
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
final matches =
widget.options.values.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
if (matches.isNotEmpty) {
for (final match in matches) {
yield IdQueryParameter.fromId(match.id);
@@ -218,33 +221,18 @@ class _FullscreenLabelFormState<T extends Label>
}
String? _buildHintText() {
if (widget.initialValue?.isSet ?? false) {
return widget.options[widget.initialValue!.id]!.name;
}
if (widget.initialValue?.onlyNotAssigned ?? false) {
return S.of(context)!.notAssigned;
}
if (widget.initialValue?.onlyAssigned ?? false) {
return S.of(context)!.anyAssigned;
}
return S.of(context)!.startTyping;
return widget.initialValue?.when(
unset: () => S.of(context)!.startTyping,
notAssigned: () => S.of(context)!.notAssigned,
anyAssigned: () => S.of(context)!.anyAssigned,
fromId: (id) => widget.options[id]!.name,
);
}
Widget _buildOptionWidget(IdQueryParameter option, bool highlight) {
void onTap() => widget.onSubmit(returnValue: option);
late final String title;
if (option.isSet) {
title = widget.options[option.id]!.name;
}
if (option.onlyNotAssigned) {
title = S.of(context)!.notAssigned;
}
if (option.onlyAssigned) {
title = S.of(context)!.anyAssigned;
}
if (option.isUnset) {
if (option.isUnset()) {
return Center(
child: Column(
children: [
@@ -258,6 +246,12 @@ class _FullscreenLabelFormState<T extends Label>
),
);
}
final title = option.whenOrNull(
notAssigned: () => S.of(context)!.notAssigned,
anyAssigned: () => S.of(context)!.anyAssigned,
fromId: (id) => widget.options[id]!.name,
)!; // Never null, since we already return on unset before
return ListTile(
selected: highlight,
selectedTileColor: Theme.of(context).focusColor,

View File

@@ -45,20 +45,19 @@ class LabelFormField<T extends Label> extends StatelessWidget {
}) : super(key: key);
String _buildText(BuildContext context, IdQueryParameter? value) {
if (value?.isSet ?? false) {
return options[value!.id]!.name;
} else if (value?.onlyNotAssigned ?? false) {
return S.of(context)!.notAssigned;
} else if (value?.onlyAssigned ?? false) {
return S.of(context)!.anyAssigned;
}
return '';
return value?.when(
unset: () => '',
notAssigned: () => S.of(context)!.notAssigned,
anyAssigned: () => S.of(context)!.anyAssigned,
fromId: (id) => options[id]!.name,
) ??
'';
}
@override
Widget build(BuildContext context) {
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
addLabelPageBuilder != null;
final isEnabled =
options.values.any((e) => (e.documentCount ?? 0) > 0) || addLabelPageBuilder != null;
return FormBuilderField<IdQueryParameter>(
name: name,
initialValue: initialValue,
@@ -68,8 +67,9 @@ class LabelFormField<T extends Label> extends StatelessWidget {
final controller = TextEditingController(
text: _buildText(context, field.value),
);
final displayedSuggestions =
suggestions.whereNot((e) => e.id == field.value?.id).toList();
final displayedSuggestions = suggestions
.whereNot((e) => e.id == field.value?.maybeWhen(fromId: (id) => id, orElse: () => -1))
.toList();
return Column(
children: [
@@ -93,8 +93,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
suffixIcon: controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () =>
field.didChange(const IdQueryParameter.unset()),
onPressed: () => field.didChange(const IdQueryParameter.unset()),
)
: null,
),
@@ -107,8 +106,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
? (initialName) {
return Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) =>
addLabelPageBuilder!(initialName),
builder: (context) => addLabelPageBuilder!(initialName),
),
);
}
@@ -139,8 +137,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
scrollDirection: Axis.horizontal,
itemCount: displayedSuggestions.length,
itemBuilder: (context, index) {
final suggestion =
displayedSuggestions.elementAt(index);
final suggestion = displayedSuggestions.elementAt(index);
return ColoredChipWrapper(
child: ActionChip(
label: Text(suggestion.name),

View File

@@ -5,8 +5,8 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
import 'package:paperless_mobile/features/login/model/user_account.dart';
import 'package:paperless_mobile/features/settings/model/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/user_account.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
class LabelItem<T extends Label> extends StatelessWidget {
@@ -46,10 +46,9 @@ class LabelItem<T extends Label> extends StatelessWidget {
onPressed: (label.documentCount ?? 0) == 0
? null
: () {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
final filter = filterBuilder(label);
Navigator.push(
context,
@@ -60,8 +59,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
context.read(),
context.read(),
context.read(),
Hive.box<UserAccount>(HiveBoxes.userAccount)
.get(currentUser)!,
Hive.box<UserAccount>(HiveBoxes.userAccount).get(currentUser)!,
),
child: const LinkedDocumentsPage(),
),