mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 16:07:58 -06:00
feat: Add hive type adapters to api models, migrate to freezed
This commit is contained in:
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user