feat: Replaced old label form fields with full page search, removed badge from edit button in document details

This commit is contained in:
Anton Stubenbord
2023-04-07 18:04:56 +02:00
parent 79ccdd0946
commit 10d48e6a55
58 changed files with 3457 additions and 487 deletions

View File

@@ -28,6 +28,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart'; import 'package:paperless_mobile/core/repository/label_repository_state.dart';
@@ -14,8 +15,8 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
Object source, { Object source, {
required void Function(LabelRepositoryState) onChanged, required void Function(LabelRepositoryState) onChanged,
}) { }) {
onChanged(state);
_subscribers.putIfAbsent(source, () { _subscribers.putIfAbsent(source, () {
onChanged(state);
return stream.listen((event) => onChanged(event)); return stream.listen((event) => onChanged(event));
}); });
} }
@@ -26,6 +27,7 @@ class LabelRepository extends HydratedCubit<LabelRepositoryState> {
} }
Future<void> initialize() { Future<void> initialize() {
debugPrint("Initializing labels...");
return Future.wait([ return Future.wait([
findAllCorrespondents(), findAllCorrespondents(),
findAllDocumentTypes(), findAllDocumentTypes(),

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -56,7 +56,9 @@ class _FormBuilderRelativeDateRangePickerState
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly, FilteringTextInputFormatter.digitsOnly,
], ],
validator: FormBuilderValidators.numeric(), // validator: (value) { //TODO: Check if this is required
// do numeric validation
// },
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) { onChanged: (value) {
final parsed = int.tryParse(value); final parsed = int.tryParse(value);

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class OfflineBanner extends StatelessWidget with PreferredSizeWidget { class OfflineBanner extends StatelessWidget implements PreferredSizeWidget {
const OfflineBanner({super.key}); const OfflineBanner({super.key});
@override @override

View File

@@ -61,9 +61,8 @@ class _BulkEditLabelBottomSheetState<T extends Label>
initialValue: initialValue:
IdQueryParameter.fromId(widget.initialValue), IdQueryParameter.fromId(widget.initialValue),
name: "labelFormField", name: "labelFormField",
labelOptions: widget.availableOptionsSelector(state), options: widget.availableOptionsSelector(state),
textFieldLabel: widget.formFieldLabel, labelText: widget.formFieldLabel,
formBuilderState: _formKey.currentState,
prefixIcon: widget.formFieldPrefixIcon, prefixIcon: widget.formFieldPrefixIcon,
), ),
), ),

View File

@@ -21,7 +21,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final DocumentChangedNotifier _notifier; final DocumentChangedNotifier _notifier;
final LocalNotificationService _notificationService; final LocalNotificationService _notificationService;
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
final List<StreamSubscription> _subscriptions = [];
DocumentDetailsCubit( DocumentDetailsCubit(
this._api, this._api,
this._labelRepository, this._labelRepository,
@@ -207,9 +207,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
@override @override
Future<void> close() async { Future<void> close() async {
for (final element in _subscriptions) { _labelRepository.removeListener(this);
await element.cancel();
}
_notifier.removeListener(this); _notifier.removeListener(this);
await super.close(); await super.close();
} }

View File

@@ -218,32 +218,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Widget _buildEditButton() { Widget _buildEditButton() {
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) { builder: (context, state) {
final _filteredSuggestions = // final _filteredSuggestions =
state.suggestions?.documentDifference(state.document); // state.suggestions?.documentDifference(state.document);
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) { builder: (context, connectivityState) {
if (!connectivityState.isConnected) { if (!connectivityState.isConnected) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return b.Badge( return Tooltip(
position: b.BadgePosition.topEnd(top: -12, end: -6), message: S.of(context)!.editDocumentTooltip,
showBadge: _filteredSuggestions?.hasSuggestions ?? false, preferBelow: false,
child: Tooltip( verticalOffset: 40,
message: S.of(context)!.editDocumentTooltip, child: FloatingActionButton(
preferBelow: false, child: const Icon(Icons.edit),
verticalOffset: 40, onPressed: () => _onEdit(state.document),
child: FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
),
), ),
badgeContent: Text(
'${_filteredSuggestions?.suggestionsCount ?? 0}',
style: const TextStyle(
color: Colors.white,
),
),
badgeColor: Colors.red,
); );
}, },
); );

View File

@@ -12,10 +12,8 @@ part 'document_edit_cubit.freezed.dart';
class DocumentEditCubit extends Cubit<DocumentEditState> { class DocumentEditCubit extends Cubit<DocumentEditState> {
final DocumentModel _initialDocument; final DocumentModel _initialDocument;
final PaperlessDocumentsApi _docsApi; final PaperlessDocumentsApi _docsApi;
final DocumentChangedNotifier _notifier;
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
final List<StreamSubscription> _subscriptions = []; final DocumentChangedNotifier _notifier;
DocumentEditCubit( DocumentEditCubit(
this._labelRepository, this._labelRepository,
@@ -23,19 +21,16 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
this._notifier, { this._notifier, {
required DocumentModel document, required DocumentModel document,
}) : _initialDocument = document, }) : _initialDocument = document,
super( super(DocumentEditState(document: document)) {
DocumentEditState(
document: document,
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
),
) {
_notifier.addListener(this, onUpdated: replace); _notifier.addListener(this, onUpdated: replace);
_labelRepository.addListener( _labelRepository.addListener(
this, this,
onChanged: (labels) => emit(state.copyWith()), onChanged: (labels) => emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
)),
); );
} }
@@ -68,10 +63,8 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
@override @override
Future<void> close() { Future<void> close() {
for (final sub in _subscriptions) {
sub.cancel();
}
_notifier.removeListener(this); _notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }
} }

View File

@@ -150,10 +150,10 @@ class __$$_DocumentEditStateCopyWithImpl<$Res>
class _$_DocumentEditState implements _DocumentEditState { class _$_DocumentEditState implements _DocumentEditState {
const _$_DocumentEditState( const _$_DocumentEditState(
{required this.document, {required this.document,
required final Map<int, Correspondent> correspondents, final Map<int, Correspondent> correspondents = const {},
required final Map<int, DocumentType> documentTypes, final Map<int, DocumentType> documentTypes = const {},
required final Map<int, StoragePath> storagePaths, final Map<int, StoragePath> storagePaths = const {},
required final Map<int, Tag> tags}) final Map<int, Tag> tags = const {}})
: _correspondents = correspondents, : _correspondents = correspondents,
_documentTypes = documentTypes, _documentTypes = documentTypes,
_storagePaths = storagePaths, _storagePaths = storagePaths,
@@ -163,6 +163,7 @@ class _$_DocumentEditState implements _DocumentEditState {
final DocumentModel document; final DocumentModel document;
final Map<int, Correspondent> _correspondents; final Map<int, Correspondent> _correspondents;
@override @override
@JsonKey()
Map<int, Correspondent> get correspondents { Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents; if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@@ -171,6 +172,7 @@ class _$_DocumentEditState implements _DocumentEditState {
final Map<int, DocumentType> _documentTypes; final Map<int, DocumentType> _documentTypes;
@override @override
@JsonKey()
Map<int, DocumentType> get documentTypes { Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes; if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@@ -179,6 +181,7 @@ class _$_DocumentEditState implements _DocumentEditState {
final Map<int, StoragePath> _storagePaths; final Map<int, StoragePath> _storagePaths;
@override @override
@JsonKey()
Map<int, StoragePath> get storagePaths { Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths; if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@@ -187,6 +190,7 @@ class _$_DocumentEditState implements _DocumentEditState {
final Map<int, Tag> _tags; final Map<int, Tag> _tags;
@override @override
@JsonKey()
Map<int, Tag> get tags { Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags; if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@@ -234,10 +238,10 @@ class _$_DocumentEditState implements _DocumentEditState {
abstract class _DocumentEditState implements DocumentEditState { abstract class _DocumentEditState implements DocumentEditState {
const factory _DocumentEditState( const factory _DocumentEditState(
{required final DocumentModel document, {required final DocumentModel document,
required final Map<int, Correspondent> correspondents, final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes, final Map<int, DocumentType> documentTypes,
required final Map<int, StoragePath> storagePaths, final Map<int, StoragePath> storagePaths,
required final Map<int, Tag> tags}) = _$_DocumentEditState; final Map<int, Tag> tags}) = _$_DocumentEditState;
@override @override
DocumentModel get document; DocumentModel get document;

View File

@@ -4,9 +4,9 @@ part of 'document_edit_cubit.dart';
class DocumentEditState with _$DocumentEditState { class DocumentEditState with _$DocumentEditState {
const factory DocumentEditState({ const factory DocumentEditState({
required DocumentModel document, required DocumentModel document,
required Map<int, Correspondent> correspondents, @Default({}) Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes, @Default({}) Map<int, DocumentType> documentTypes,
required Map<int, StoragePath> storagePaths, @Default({}) Map<int, StoragePath> storagePaths,
required Map<int, Tag> tags, @Default({}) Map<int, Tag> tags,
}) = _DocumentEditState; }) = _DocumentEditState;
} }

View File

@@ -1,10 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
@@ -56,6 +57,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DocumentEditCubit, DocumentEditState>( return BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) { builder: (context, state) {
log("Updated state. correspondents have ${state.correspondents.length} items.");
return DefaultTabController( return DefaultTabController(
length: 2, length: 2,
child: Scaffold( child: Scaffold(
@@ -95,18 +97,116 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
_buildTitleFormField(state.document.title).padded(), _buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created) _buildCreatedAtFormField(state.document.created)
.padded(), .padded(),
_buildCorrespondentFormField( // Correspondent form field
state.document.correspondent, Column(
state.correspondents, children: [
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(
initialName: initialValue,
),
),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options: context
.watch<DocumentEditCubit>()
.state
.correspondents,
initialValue: IdQueryParameter.fromId(
state.document.correspondent,
),
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
),
if (_filteredSuggestions
?.hasSuggestedCorrespondents ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
_filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.correspondents[itemData]!.name),
onPressed: () {
_formKey
.currentState?.fields[fkCorrespondent]
?.didChange(
IdQueryParameter.fromId(itemData),
);
},
),
),
],
).padded(), ).padded(),
_buildDocumentTypeFormField( // DocumentType form field
state.document.documentType, Column(
state.documentTypes, children: [
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (currentInput) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue: IdQueryParameter.fromId(
state.document.documentType),
options: state.documentTypes,
name: _DocumentEditPageState.fkDocumentType,
prefixIcon:
const Icon(Icons.description_outlined),
),
if (_filteredSuggestions
?.hasSuggestedDocumentTypes ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
_filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.documentTypes[itemData]!.name),
onPressed: () => _formKey
.currentState?.fields[fkDocumentType]
?.didChange(
IdQueryParameter.fromId(itemData),
),
),
),
],
).padded(), ).padded(),
_buildStoragePathFormField( // StoragePath form field
state.document.storagePath, Column(
state.storagePaths, children: [
LabelFormField<StoragePath>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(
initalName: initialValue),
),
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
initialValue: IdQueryParameter.fromId(
state.document.storagePath),
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
),
],
).padded(), ).padded(),
// Tag form field
TagFormField( TagFormField(
initialValue: IdsTagsQuery.included( initialValue: IdsTagsQuery.included(
state.document.tags.toList()), state.document.tags.toList()),
@@ -187,96 +287,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
); );
} }
Widget _buildStoragePathFormField(
int? initialId,
Map<int, StoragePath> options,
) {
return Column(
children: [
LabelFormField<StoragePath>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(initalName: initialValue),
),
textFieldLabel: S.of(context)!.storagePath,
labelOptions: options,
initialValue: IdQueryParameter.fromId(initialId),
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
),
],
);
}
Widget _buildCorrespondentFormField(
int? initialId, Map<int, Correspondent> options) {
return Column(
children: [
LabelFormField<Correspondent>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(initialName: initialValue),
),
textFieldLabel: S.of(context)!.correspondent,
labelOptions: options,
initialValue: IdQueryParameter.fromId(initialId),
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
),
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false)
_buildSuggestionsSkeleton<int>(
suggestions: _filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkCorrespondent]
?.didChange((IdQueryParameter.fromId(itemData))),
),
),
],
);
}
Widget _buildDocumentTypeFormField(
int? initialId,
Map<int, DocumentType> options,
) {
return Column(
children: [
LabelFormField<DocumentType>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
textFieldLabel: S.of(context)!.documentType,
initialValue: IdQueryParameter.fromId(initialId),
labelOptions: options,
name: fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined),
),
if (_filteredSuggestions?.hasSuggestedDocumentTypes ?? false)
_buildSuggestionsSkeleton<int>(
suggestions: _filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
?.didChange(IdQueryParameter.fromId(itemData)),
),
),
],
);
}
Future<void> _onSubmit(DocumentModel document) async { Future<void> _onSubmit(DocumentModel document) async {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value; final values = _formKey.currentState!.value;
@@ -308,7 +318,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
Widget _buildTitleFormField(String? initialTitle) { Widget _buildTitleFormField(String? initialTitle) {
return FormBuilderTextField( return FormBuilderTextField(
name: fkTitle, name: fkTitle,
validator: FormBuilderValidators.required(), validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
}
return null;
},
decoration: InputDecoration( decoration: InputDecoration(
label: Text(S.of(context)!.title), label: Text(S.of(context)!.title),
), ),
@@ -374,3 +389,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
).padded(); ).padded();
} }
} }
// class SampleWidget extends StatefulWidget {
// const SampleWidget({super.key});
// @override
// State<SampleWidget> createState() => _SampleWidgetState();
// }
// class _SampleWidgetState extends State<SampleWidget> {
// @override
// Widget build(BuildContext context) {
// return BlocBuilder<OptionsBloc, OptionsState>(
// builder: (context, state) {
// return OptionsFormField(
// options: state.options,
// onAddOption: (option) {
// // This will call the repository and will cause a new state containing the new option to be emitted.
// context.read<OptionsBloc>().addOption(option);
// },
// );
// },
// );
// }
// }
// class OptionsFormField extends StatefulWidget {
// final List<Option> options;
// final void Function(Option option) onAddOption;
// const OptionsFormField({
// super.key,
// required this.options,
// required this.onAddOption,
// });
// @override
// State<OptionsFormField> createState() => _OptionsFormFieldState();
// }
// class _OptionsFormFieldState extends State<OptionsFormField> {
// final TextEditingController _controller;
// @override
// Widget build(BuildContext context) {
// return TextFormField(
// onTap: () async {
// // User creates new option...
// final Option option = await showOptionCreationForm();
// widget.onAddOption(option);
// },
// );
// }
// }

View File

@@ -23,14 +23,19 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
DocumentSearchCubit(this.api, this.notifier, this._labelRepository) DocumentSearchCubit(this.api, this.notifier, this._labelRepository)
: super(const DocumentSearchState()) { : super(const DocumentSearchState()) {
_labelRepository.addListener(this, onChanged: (labels) { _labelRepository.addListener(
emit(state.copyWith( this,
correspondents: labels.correspondents, onChanged: (labels) {
documentTypes: labels.documentTypes, emit(
tags: labels.tags, state.copyWith(
storagePaths: labels.storagePaths, correspondents: labels.correspondents,
)); documentTypes: labels.documentTypes,
}); tags: labels.tags,
storagePaths: labels.storagePaths,
),
);
},
);
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
@@ -101,6 +106,7 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
@override @override
Future<void> close() { Future<void> close() {
notifier.removeListener(this); notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }

View File

@@ -3,7 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
as s;
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -27,7 +28,7 @@ class SliverSearchBar extends StatelessWidget {
maxExtent: kToolbarHeight, maxExtent: kToolbarHeight,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0), margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: SearchBar( child: s.SearchBar(
height: kToolbarHeight, height: kToolbarHeight,
supportingText: S.of(context)!.searchDocuments, supportingText: S.of(context)!.searchDocuments,
onTap: () => showDocumentSearchPage(context), onTap: () => showDocumentSearchPage(context),

View File

@@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -97,7 +97,12 @@ class _DocumentUploadPreparationPageState
name: DocumentModel.titleKey, name: DocumentModel.titleKey,
initialValue: initialValue:
widget.title ?? "scan_${fileNameDateFormat.format(_now)}", widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
validator: FormBuilderValidators.required(), validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
}
return null;
},
decoration: InputDecoration( decoration: InputDecoration(
labelText: S.of(context)!.title, labelText: S.of(context)!.title,
suffixIcon: IconButton( suffixIcon: IconButton(
@@ -189,30 +194,32 @@ class _DocumentUploadPreparationPageState
), ),
// Correspondent // Correspondent
LabelFormField<Correspondent>( LabelFormField<Correspondent>(
notAssignedSelectable: false, showAnyAssignedOption: false,
formBuilderState: _formKey.currentState, showNotAssignedOption: false,
labelCreationWidgetBuilder: (initialName) => addLabelPageBuilder: (initialName) =>
RepositoryProvider.value( RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddCorrespondentPage(initialName: initialName), child: AddCorrespondentPage(initialName: initialName),
), ),
textFieldLabel: S.of(context)!.correspondent + " *", addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey, name: DocumentModel.correspondentKey,
labelOptions: state.correspondents, options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
), ),
// Document type // Document type
LabelFormField<DocumentType>( LabelFormField<DocumentType>(
notAssignedSelectable: false, showAnyAssignedOption: false,
formBuilderState: _formKey.currentState, showNotAssignedOption: false,
labelCreationWidgetBuilder: (initialName) => addLabelPageBuilder: (initialName) =>
RepositoryProvider.value( RepositoryProvider.value(
value: context.read<LabelRepository>(), value: context.read<LabelRepository>(),
child: AddDocumentTypePage(initialName: initialName), child: AddDocumentTypePage(initialName: initialName),
), ),
textFieldLabel: S.of(context)!.documentType + " *", addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey, name: DocumentModel.documentTypeKey,
labelOptions: state.documentTypes, options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
), ),
TagFormField( TagFormField(

View File

@@ -24,12 +24,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
DocumentsCubit(this.api, this.notifier, this._labelRepository) DocumentsCubit(this.api, this.notifier, this._labelRepository)
: super(DocumentsState( : super(const DocumentsState()) {
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
)) {
notifier.addListener( notifier.addListener(
this, this,
onUpdated: (document) { onUpdated: (document) {

View File

@@ -1,15 +1,19 @@
part of 'documents_cubit.dart'; part of 'documents_cubit.dart';
@JsonSerializable(ignoreUnannotated: true) @JsonSerializable()
class DocumentsState extends DocumentPagingState { class DocumentsState extends DocumentPagingState {
@JsonKey(includeToJson: false, includeFromJson: false)
final List<DocumentModel> selection; final List<DocumentModel> selection;
@JsonKey(includeToJson: false, includeFromJson: false)
final Map<int, Correspondent> correspondents; final Map<int, Correspondent> correspondents;
@JsonKey(includeToJson: false, includeFromJson: false)
final Map<int, DocumentType> documentTypes; final Map<int, DocumentType> documentTypes;
@JsonKey(includeToJson: false, includeFromJson: false)
final Map<int, Tag> tags; final Map<int, Tag> tags;
@JsonKey(includeToJson: false, includeFromJson: false)
final Map<int, StoragePath> storagePaths; final Map<int, StoragePath> storagePaths;
@JsonKey()
final ViewType viewType; final ViewType viewType;
const DocumentsState({ const DocumentsState({
@@ -53,15 +57,14 @@ class DocumentsState extends DocumentPagingState {
); );
} }
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
_$DocumentsStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
@override @override
List<Object?> get props => [ List<Object?> get props => [
selection, selection,
viewType, viewType,
correspondents,
documentTypes,
tags,
storagePaths,
...super.props, ...super.props,
]; ];
@@ -79,4 +82,9 @@ class DocumentsState extends DocumentPagingState {
value: value, value: value,
); );
} }
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
_$DocumentsStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
} }

View File

@@ -145,7 +145,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
Widget _buildFullView(BuildContext context) { Widget _buildFullView(BuildContext context) {
if (showLoadingPlaceholder) { if (showLoadingPlaceholder) {
//TODO: Build detailed loading animation //TODO: Build detailed loading animation
return DocumentsListLoadingWidget.sliver(); return const DocumentsListLoadingWidget.sliver();
} }
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(

View File

@@ -11,7 +11,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d
class DocumentListItem extends DocumentItem { class DocumentListItem extends DocumentItem {
static const _a4AspectRatio = 1 / 1.4142; static const _a4AspectRatio = 1 / 1.4142;
const DocumentListItem({ DocumentListItem({
super.key, super.key,
required super.document, required super.document,
required super.isSelected, required super.isSelected,
@@ -28,7 +28,9 @@ class DocumentListItem extends DocumentItem {
required super.correspondents, required super.correspondents,
required super.documentTypes, required super.documentTypes,
required super.storagePaths, required super.storagePaths,
}); }) {
print(tags.keys.join(", "));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -64,11 +66,14 @@ class DocumentListItem extends DocumentItem {
absorbing: isSelectionActive, absorbing: isSelectionActive,
child: TagsWidget( child: TagsWidget(
isClickable: isLabelClickable, isClickable: isLabelClickable,
tags: document.tags.map((e) => tags[e]!).toList(), tags: document.tags
.where((e) => tags.containsKey(e))
.map((e) => tags[e]!)
.toList(),
isMultiLine: false, isMultiLine: false,
onTagSelected: (id) => onTagSelected?.call(id), onTagSelected: (id) => onTagSelected?.call(id),
), ),
) ),
], ],
), ),
subtitle: Padding( subtitle: Padding(

View File

@@ -155,10 +155,9 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
Widget _buildDocumentTypeFormField() { Widget _buildDocumentTypeFormField() {
return LabelFormField<DocumentType>( return LabelFormField<DocumentType>(
formBuilderState: widget.formKey.currentState,
name: DocumentFilterForm.fkDocumentType, name: DocumentFilterForm.fkDocumentType,
labelOptions: widget.documentTypes, options: widget.documentTypes,
textFieldLabel: S.of(context)!.documentType, labelText: S.of(context)!.documentType,
initialValue: widget.initialFilter.documentType, initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
); );
@@ -166,10 +165,9 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
Widget _buildCorrespondentFormField() { Widget _buildCorrespondentFormField() {
return LabelFormField<Correspondent>( return LabelFormField<Correspondent>(
formBuilderState: widget.formKey.currentState,
name: DocumentFilterForm.fkCorrespondent, name: DocumentFilterForm.fkCorrespondent,
labelOptions: widget.correspondents, options: widget.correspondents,
textFieldLabel: S.of(context)!.correspondent, labelText: S.of(context)!.correspondent,
initialValue: widget.initialFilter.correspondent, initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
); );
@@ -177,10 +175,9 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
Widget _buildStoragePathFormField() { Widget _buildStoragePathFormField() {
return LabelFormField<StoragePath>( return LabelFormField<StoragePath>(
formBuilderState: widget.formKey.currentState,
name: DocumentFilterForm.fkStoragePath, name: DocumentFilterForm.fkStoragePath,
labelOptions: widget.storagePaths, options: widget.storagePaths,
textFieldLabel: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
initialValue: widget.initialFilter.storagePath, initialValue: widget.initialFilter.storagePath,
prefixIcon: const Icon(Icons.folder_outlined), prefixIcon: const Icon(Icons.folder_outlined),
); );

View File

@@ -8,7 +8,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'edit_label_state.dart'; part 'edit_label_state.dart';
part 'edit_label_cubit.freezed.dart'; part 'edit_label_cubit.freezed.dart';
class EditLabelCubit extends Cubit<EditLabelState> with LabelCubitMixin { class EditLabelCubit extends Cubit<EditLabelState>
with LabelCubitMixin<EditLabelState> {
@override @override
final LabelRepository labelRepository; final LabelRepository labelRepository;
@@ -29,16 +30,4 @@ class EditLabelCubit extends Cubit<EditLabelState> with LabelCubitMixin {
labelRepository.removeListener(this); labelRepository.removeListener(this);
return super.close(); return super.close();
} }
@override
Map<int, Correspondent> get correspondents => state.correspondents;
@override
Map<int, DocumentType> get documentTypes => state.documentTypes;
@override
Map<int, StoragePath> get storagePaths => state.storagePaths;
@override
Map<int, Tag> get tags => state.tags;
} }

View File

@@ -62,6 +62,7 @@ class AddLabelFormWidget<T extends Label> extends StatelessWidget {
title: pageTitle, title: pageTitle,
), ),
body: LabelForm<T>( body: LabelForm<T>(
autofocusNameField: true,
initialValue: label, initialValue: label,
fromJsonT: fromJsonT, fromJsonT: fromJsonT,
submitButtonConfig: SubmitButtonConfig<T>( submitButtonConfig: SubmitButtonConfig<T>(

View File

@@ -72,6 +72,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
], ],
), ),
body: LabelForm<T>( body: LabelForm<T>(
autofocusNameField: false,
initialValue: label, initialValue: label,
fromJsonT: fromJsonT, fromJsonT: fromJsonT,
submitButtonConfig: SubmitButtonConfig<T>( submitButtonConfig: SubmitButtonConfig<T>(

View File

@@ -11,17 +11,20 @@ class EditCorrespondentPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
lazy: false,
create: (context) => EditLabelCubit( create: (context) => EditLabelCubit(
context.read(), context.read(),
), ),
child: EditLabelPage<Correspondent>( child: Builder(builder: (context) {
label: correspondent, return EditLabelPage<Correspondent>(
fromJsonT: Correspondent.fromJson, label: correspondent,
onSubmit: (context, label) => fromJsonT: Correspondent.fromJson,
context.read<EditLabelCubit>().addCorrespondent(label), onSubmit: (context, label) =>
onDelete: (context, label) => context.read<EditLabelCubit>().replaceCorrespondent(label),
context.read<EditLabelCubit>().removeCorrespondent(label), onDelete: (context, label) =>
), context.read<EditLabelCubit>().removeCorrespondent(label),
);
}),
); );
} }
} }

View File

@@ -19,7 +19,7 @@ class EditDocumentTypePage extends StatelessWidget {
label: documentType, label: documentType,
fromJsonT: DocumentType.fromJson, fromJsonT: DocumentType.fromJson,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().addDocumentType(label), context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label), context.read<EditLabelCubit>().removeDocumentType(label),
), ),

View File

@@ -19,7 +19,7 @@ class EditStoragePathPage extends StatelessWidget {
label: storagePath, label: storagePath,
fromJsonT: StoragePath.fromJson, fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label), context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label), context.read<EditLabelCubit>().removeStoragePath(label),
additionalFields: [ additionalFields: [

View File

@@ -22,7 +22,7 @@ class EditTagPage extends StatelessWidget {
label: tag, label: tag,
fromJsonT: Tag.fromJson, fromJsonT: Tag.fromJson,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().addTag(label), context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label), context.read<EditLabelCubit>().removeTag(label),
additionalFields: [ additionalFields: [

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
@@ -25,7 +25,7 @@ class SubmitButtonConfig<T extends Label> {
class LabelForm<T extends Label> extends StatefulWidget { class LabelForm<T extends Label> extends StatefulWidget {
final T? initialValue; final T? initialValue;
final SubmitButtonConfig submitButtonConfig; final SubmitButtonConfig<T> submitButtonConfig;
/// FromJson method to parse the form field values into a label instance. /// FromJson method to parse the form field values into a label instance.
final T Function(Map<String, dynamic> json) fromJsonT; final T Function(Map<String, dynamic> json) fromJsonT;
@@ -33,12 +33,15 @@ class LabelForm<T extends Label> extends StatefulWidget {
/// List of additionally rendered form fields. /// List of additionally rendered form fields.
final List<Widget> additionalFields; final List<Widget> additionalFields;
final bool autofocusNameField;
const LabelForm({ const LabelForm({
Key? key, Key? key,
required this.initialValue, required this.initialValue,
required this.fromJsonT, required this.fromJsonT,
this.additionalFields = const [], this.additionalFields = const [],
required this.submitButtonConfig, required this.submitButtonConfig,
required this.autofocusNameField,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -74,12 +77,18 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
child: ListView( child: ListView(
children: [ children: [
FormBuilderTextField( FormBuilderTextField(
autofocus: widget.autofocusNameField,
name: Label.nameKey, name: Label.nameKey,
decoration: InputDecoration( decoration: InputDecoration(
labelText: S.of(context)!.name, labelText: S.of(context)!.name,
errorText: _errors[Label.nameKey], errorText: _errors[Label.nameKey],
), ),
validator: FormBuilderValidators.required(), validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
}
return null;
},
initialValue: widget.initialValue?.name, initialValue: widget.initialValue?.name,
onChanged: (val) => setState(() => _errors = {}), onChanged: (val) => setState(() => _errors = {}),
), ),
@@ -148,6 +157,8 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
Navigator.pop(context, createdLabel); Navigator.pop(context, createdLabel);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (errors) {
setState(() => _errors = errors);
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -75,6 +76,7 @@ class InboxCubit extends HydratedCubit<InboxState>
/// Fetches inbox tag ids and loads the inbox items (documents). /// Fetches inbox tag ids and loads the inbox items (documents).
/// ///
Future<void> loadInbox() async { Future<void> loadInbox() async {
debugPrint("Initializing inbox...");
final inboxTags = await _labelRepository.findAllTags().then( final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
); );

View File

@@ -199,14 +199,16 @@ class _InboxPageState extends State<InboxPage>
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(S.of(context)!.cancel), child: Text(
S.of(context)!.cancel,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text( child: Text(S.of(context)!.ok),
S.of(context)!.ok,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
), ),
], ],
), ),

View File

@@ -30,16 +30,4 @@ class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
labelRepository.removeListener(this); labelRepository.removeListener(this);
return super.close(); return super.close();
} }
@override
Map<int, Correspondent> get correspondents => state.correspondents;
@override
Map<int, DocumentType> get documentTypes => state.documentTypes;
@override
Map<int, StoragePath> get storagePaths => state.storagePaths;
@override
Map<int, Tag> get tags => state.tags;
} }

View File

@@ -1,15 +1,14 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
///
/// Mixin which adds functionality to manage labels to [Bloc]s.
///
mixin LabelCubitMixin<T> on BlocBase<T> { mixin LabelCubitMixin<T> on BlocBase<T> {
LabelRepository get labelRepository; LabelRepository get labelRepository;
Map<int, Correspondent> get correspondents;
Map<int, DocumentType> get documentTypes;
Map<int, Tag> get tags;
Map<int, StoragePath> get storagePaths;
Future<Correspondent> addCorrespondent(Correspondent item) async { Future<Correspondent> addCorrespondent(Correspondent item) async {
assert(item.id == null); assert(item.id == null);
final addedItem = await labelRepository.createCorrespondent(item); final addedItem = await labelRepository.createCorrespondent(item);
@@ -28,7 +27,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
Future<void> removeCorrespondent(Correspondent item) async { Future<void> removeCorrespondent(Correspondent item) async {
assert(item.id != null); assert(item.id != null);
if (correspondents.containsKey(item.id)) { if (labelRepository.state.correspondents.containsKey(item.id)) {
await labelRepository.deleteCorrespondent(item); await labelRepository.deleteCorrespondent(item);
} }
} }
@@ -51,7 +50,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
Future<void> removeDocumentType(DocumentType item) async { Future<void> removeDocumentType(DocumentType item) async {
assert(item.id != null); assert(item.id != null);
if (documentTypes.containsKey(item.id)) { if (labelRepository.state.documentTypes.containsKey(item.id)) {
await labelRepository.deleteDocumentType(item); await labelRepository.deleteDocumentType(item);
} }
} }
@@ -74,7 +73,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
Future<void> removeStoragePath(StoragePath item) async { Future<void> removeStoragePath(StoragePath item) async {
assert(item.id != null); assert(item.id != null);
if (storagePaths.containsKey(item.id)) { if (labelRepository.state.storagePaths.containsKey(item.id)) {
await labelRepository.deleteStoragePath(item); await labelRepository.deleteStoragePath(item);
} }
} }
@@ -97,7 +96,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
Future<void> removeTag(Tag item) async { Future<void> removeTag(Tag item) async {
assert(item.id != null); assert(item.id != null);
if (tags.containsKey(item.id)) { if (labelRepository.state.tags.containsKey(item.id)) {
await labelRepository.deleteTag(item); await labelRepository.deleteTag(item);
} }
} }

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -46,7 +46,12 @@ class _StoragePathAutofillFormBuilderFieldState
children: [ children: [
TextFormField( TextFormField(
controller: _textEditingController, controller: _textEditingController,
validator: FormBuilderValidators.required(), //TODO: INTL validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
}
return null;
},
decoration: InputDecoration( decoration: InputDecoration(
label: Text(S.of(context)!.storagePath), label: Text(S.of(context)!.storagePath),
suffixIcon: _showClearIcon suffixIcon: _showClearIcon

View File

@@ -0,0 +1,264 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class FullscreenLabelForm<T extends Label> extends StatefulWidget {
final IdQueryParameter? initialValue;
final Map<int, T> options;
final Future<T?> Function(String? initialName)? onCreateNewLabel;
final bool showNotAssignedOption;
final bool showAnyAssignedOption;
final void Function({IdQueryParameter returnValue}) onSubmit;
final Widget leadingIcon;
final String? addNewLabelText;
FullscreenLabelForm({
super.key,
this.initialValue,
required this.options,
required this.onCreateNewLabel,
this.showNotAssignedOption = true,
this.showAnyAssignedOption = true,
required this.onSubmit,
required this.leadingIcon,
this.addNewLabelText,
}) : assert(
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
),
assert(
!(initialValue?.onlyNotAssigned ?? false) || showNotAssignedOption,
),
assert((addNewLabelText != null) == (onCreateNewLabel != null));
@override
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState();
}
class _FullscreenLabelFormState<T extends Label>
extends State<FullscreenLabelForm<T>> {
late bool _showClearIcon = false;
final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
@override
void initState() {
super.initState();
_textEditingController.addListener(() => setState(() {
_showClearIcon = _textEditingController.text.isNotEmpty;
}));
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//Delay keyboard popup to ensure open animation is finished before.
Future.delayed(
const Duration(milliseconds: 200),
() => _focusNode.requestFocus(),
);
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final options = _filterOptionsByQuery(_textEditingController.text);
return Scaffold(
appBar: AppBar(
backgroundColor: theme.colorScheme.surface,
toolbarHeight: 72,
leading: BackButton(
color: theme.colorScheme.onSurface,
),
title: TextFormField(
focusNode: _focusNode,
controller: _textEditingController,
onFieldSubmitted: (value) {
FocusScope.of(context).unfocus();
final index = AutocompleteHighlightedOption.of(context);
final value = index.isNegative ? null : options.elementAt(index);
widget.onSubmit(returnValue: IdQueryParameter.fromId(value?.id));
},
autofocus: true,
style: theme.textTheme.bodyLarge?.apply(
color: theme.colorScheme.onSurface,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
hintStyle: theme.textTheme.bodyLarge?.apply(
color: theme.colorScheme.onSurfaceVariant,
),
icon: widget.leadingIcon,
hintText: _buildHintText(),
border: InputBorder.none,
),
textInputAction: TextInputAction.done,
),
actions: [
if (_showClearIcon)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_textEditingController.clear();
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Divider(
color: theme.colorScheme.outline,
),
),
),
body: Builder(
builder: (context) {
return Column(
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
final highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0,
);
});
}
return _buildOptionWidget(option, highlight);
},
),
),
],
);
},
),
);
}
void _onCreateNewLabel() async {
final label = await widget.onCreateNewLabel!(_textEditingController.text);
if (label?.id != null) {
widget.onSubmit(
returnValue: IdQueryParameter.fromId(label!.id!),
);
}
}
///
/// Filters the options passed to this widget by the current [query] and
/// adds not-/any assigned options
///
Iterable<IdQueryParameter> _filterOptionsByQuery(String query) sync* {
final normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.isEmpty) {
if (widget.initialValue == null) {
// If nothing is selected yet, show all options first.
for (final option in widget.options.values) {
yield IdQueryParameter.fromId(option.id);
}
if (widget.showNotAssignedOption) {
yield const IdQueryParameter.notAssigned();
}
if (widget.showAnyAssignedOption) {
yield const IdQueryParameter.anyAssigned();
}
} else {
// If an initial value is given, show not assigned first, which will be selected by default when pressing "done" on keyboard.
if (widget.showNotAssignedOption) {
yield const IdQueryParameter.notAssigned();
}
if (widget.showAnyAssignedOption) {
yield const IdQueryParameter.anyAssigned();
}
for (final option in widget.options.values) {
// Don't include the initial value in the selection
if (option.id == widget.initialValue?.id) {
continue;
}
yield IdQueryParameter.fromId(option.id);
}
}
} 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));
if (matches.isNotEmpty) {
for (final match in matches) {
yield IdQueryParameter.fromId(match.id);
}
if (widget.showNotAssignedOption) {
yield const IdQueryParameter.notAssigned();
}
if (widget.showAnyAssignedOption) {
yield const IdQueryParameter.anyAssigned();
}
} else {
if (widget.showNotAssignedOption) {
yield const IdQueryParameter.notAssigned();
}
if (widget.showAnyAssignedOption) {
yield const IdQueryParameter.anyAssigned();
}
if (!(widget.showAnyAssignedOption || widget.showNotAssignedOption)) {
yield const IdQueryParameter.unset();
}
}
}
}
String? _buildHintText() {
if (widget.initialValue?.isSet ?? false) {
return widget.options[widget.initialValue!.id]?.name ?? 'undefined';
}
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;
}
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) {
return Center(
child: Column(
children: [
Text(S.of(context)!.noItemsFound).padded(),
if (widget.onCreateNewLabel != null)
TextButton(
child: Text(widget.addNewLabelText!),
onPressed: _onCreateNewLabel,
),
],
),
);
}
return ListTile(
selected: highlight,
selectedTileColor: Theme.of(context).focusColor,
title: Text(title),
onTap: onTap,
);
}
}

View File

@@ -1,195 +1,164 @@
import 'dart:developer';
import 'package:animations/animations.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_type_ahead.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/view/widgets/fullscreen_label_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
/// ///
/// Form field allowing to select labels (i.e. correspondent, documentType) /// Form field allowing to select labels (i.e. correspondent, documentType)
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...). /// [T] is the label type (e.g. [DocumentType], [Correspondent], ...)
/// ///
class LabelFormField<T extends Label> extends StatefulWidget { class LabelFormField<T extends Label> extends StatelessWidget {
final Widget prefixIcon; final Widget prefixIcon;
final Map<int, T> labelOptions; final Map<int, T> options;
final FormBuilderState? formBuilderState;
final IdQueryParameter? initialValue; final IdQueryParameter? initialValue;
final String name; final String name;
final String textFieldLabel; final String labelText;
final FormFieldValidator? validator; final FormFieldValidator? validator;
final Widget Function(String initialName)? labelCreationWidgetBuilder; final Widget Function(String? initialName)? addLabelPageBuilder;
final bool notAssignedSelectable;
final void Function(IdQueryParameter?)? onChanged; final void Function(IdQueryParameter?)? onChanged;
final bool showNotAssignedOption;
final bool showAnyAssignedOption;
final List<T> suggestions;
final String? addLabelText;
const LabelFormField({ const LabelFormField({
Key? key, Key? key,
required this.name, required this.name,
required this.labelOptions, required this.options,
this.validator, required this.labelText,
this.initialValue,
required this.textFieldLabel,
this.labelCreationWidgetBuilder,
required this.formBuilderState,
required this.prefixIcon, required this.prefixIcon,
this.notAssignedSelectable = true, this.initialValue,
this.validator,
this.addLabelPageBuilder,
this.onChanged, this.onChanged,
this.showNotAssignedOption = true,
this.showAnyAssignedOption = true,
this.suggestions = const [],
this.addLabelText,
}) : super(key: key); }) : super(key: key);
@override String _buildText(BuildContext context, IdQueryParameter? value) {
State<LabelFormField<T>> createState() => _LabelFormFieldState<T>(); if (value?.isSet ?? false) {
} return options[value!.id]?.name ?? 'undefined';
} else if (value?.onlyNotAssigned ?? false) {
class _LabelFormFieldState<T extends Label> extends State<LabelFormField<T>> { return S.of(context)!.notAssigned;
bool _showCreationSuffixIcon = false; } else if (value?.onlyAssigned ?? false) {
late bool _showClearSuffixIcon; return S.of(context)!.anyAssigned;
}
late final TextEditingController _textEditingController; return '';
@override
void initState() {
super.initState();
_showClearSuffixIcon =
widget.labelOptions.containsKey(widget.initialValue?.id);
_textEditingController = TextEditingController(
text: widget.labelOptions[widget.initialValue?.id]?.name ?? '',
)..addListener(() {
setState(() {
_showCreationSuffixIcon = widget.labelOptions.values
.where(
(item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(),
),
)
.isEmpty;
});
setState(() =>
_showClearSuffixIcon = _textEditingController.text.isNotEmpty);
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEnabled = widget.labelOptions.values.fold<bool>( final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
false, addLabelPageBuilder != null;
(previousValue, element) => return FormBuilderField<IdQueryParameter>(
previousValue || (element.documentCount ?? 0) > 0) || name: name,
widget.labelCreationWidgetBuilder != null; initialValue: initialValue,
return FormBuilderTypeAhead<IdQueryParameter>( onChanged: onChanged,
enabled: isEnabled, enabled: isEnabled,
noItemsFoundBuilder: (context) => Padding( builder: (field) {
padding: const EdgeInsets.symmetric(vertical: 8), final controller = TextEditingController(
child: Text( text: _buildText(context, field.value),
S.of(context)!.noItemsFound, );
textAlign: TextAlign.center, final displayedSuggestions =
style: suggestions.whereNot((e) => e.id == field.value?.id).toList();
TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0),
),
),
loadingBuilder: (context) => Container(),
initialValue: widget.initialValue ?? const IdQueryParameter.unset(),
name: widget.name,
suggestionsBoxDecoration: SuggestionsBoxDecoration(
elevation: 4.0,
shadowColor: Theme.of(context).colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
itemBuilder: (context, suggestion) => ListTile(
title: Text(
widget.labelOptions[suggestion.id]?.name ??
S.of(context)!.notAssigned,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// tileColor: Theme.of(context).colorScheme.surfaceVariant,
dense: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
suggestionsCallback: (pattern) {
final List<IdQueryParameter> suggestions = widget.labelOptions.entries
.where(
(entry) =>
widget.labelOptions[entry.key]!.name
.toLowerCase()
.contains(pattern.toLowerCase()) ||
pattern.isEmpty,
)
.where(
(entry) =>
widget.labelCreationWidgetBuilder != null ||
(entry.value.documentCount ?? 0) > 0,
)
.map((entry) => IdQueryParameter.fromId(entry.key))
.toList();
if (widget.notAssignedSelectable) {
suggestions.insert(0, const IdQueryParameter.notAssigned());
}
return suggestions;
},
onChanged: (value) {
setState(() => _showClearSuffixIcon = value?.isSet ?? false);
widget.onChanged?.call(value);
},
controller: _textEditingController,
decoration: InputDecoration(
prefixIcon: widget.prefixIcon,
label: Text(widget.textFieldLabel),
hintText: S.of(context)!.startTyping,
suffixIcon: _buildSuffixIcon(context),
),
selectionToTextTransformer: (suggestion) {
if (suggestion == const IdQueryParameter.notAssigned()) {
return S.of(context)!.notAssigned;
}
return widget.labelOptions[suggestion.id]?.name ?? "";
},
direction: AxisDirection.up,
onSuggestionSelected: (suggestion) =>
widget.formBuilderState?.fields[widget.name]?.didChange(suggestion),
);
}
Widget? _buildSuffixIcon(BuildContext context) { return Column(
if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) { children: [
return IconButton( OpenContainer<IdQueryParameter>(
onPressed: () async { middleColor: Theme.of(context).colorScheme.background,
FocusScope.of(context).unfocus(); closedColor: Theme.of(context).colorScheme.background,
final createdLabel = await showDialog<T>( openColor: Theme.of(context).colorScheme.background,
context: context, closedShape: InputBorder.none,
builder: (context) => widget.labelCreationWidgetBuilder!( openElevation: 0,
_textEditingController.text, closedElevation: 0,
closedBuilder: (context, openForm) => Container(
margin: const EdgeInsets.only(top: 4),
child: TextField(
controller: controller,
onTap: openForm,
readOnly: true,
enabled: isEnabled,
decoration: InputDecoration(
prefixIcon: prefixIcon,
labelText: labelText,
suffixIcon: controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () =>
field.didChange(const IdQueryParameter.unset()),
)
: null,
),
),
),
openBuilder: (context, closeForm) => FullscreenLabelForm<T>(
addNewLabelText: addLabelText,
leadingIcon: prefixIcon,
onCreateNewLabel: addLabelPageBuilder != null
? (initialName) {
return Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) =>
addLabelPageBuilder!(initialName),
),
);
}
: null,
options: options,
onSubmit: closeForm,
initialValue: field.value,
showAnyAssignedOption: showAnyAssignedOption,
showNotAssignedOption: showNotAssignedOption,
),
onClosed: (data) {
if (data != null) {
field.didChange(data);
}
},
), ),
); if (displayedSuggestions.isNotEmpty)
if (createdLabel != null) { Column(
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done). crossAxisAlignment: CrossAxisAlignment.start,
widget.formBuilderState?.fields[widget.name] children: [
?.didChange(IdQueryParameter.fromId(createdLabel.id)); Text(
_textEditingController.text = createdLabel.name; S.of(context)!.suggestions,
} else { style: Theme.of(context).textTheme.bodySmall,
_reset(); ),
} SizedBox(
}, height: 48,
icon: const Icon( child: ListView.separated(
Icons.new_label, scrollDirection: Axis.horizontal,
), itemCount: displayedSuggestions.length,
); itemBuilder: (context, index) {
} final suggestion =
if (_showClearSuffixIcon) { displayedSuggestions.elementAt(index);
return IconButton( return ColoredChipWrapper(
icon: const Icon(Icons.clear), child: ActionChip(
onPressed: _reset, label: Text(suggestion.name),
); onPressed: () => field.didChange(
} IdQueryParameter.fromId(suggestion.id),
return null; ),
} ),
);
void _reset() { },
widget.formBuilderState?.fields[widget.name]?.didChange( separatorBuilder: (BuildContext context, int index) =>
const IdQueryParameter.unset(), const SizedBox(width: 4.0),
),
),
],
).padded(),
],
);
},
); );
_textEditingController.clear();
} }
} }

View File

@@ -64,6 +64,10 @@ class LinkedDocumentsState extends DocumentPagingState {
@override @override
List<Object?> get props => [ List<Object?> get props => [
viewType, viewType,
correspondents,
documentTypes,
tags,
storagePaths,
...super.props, ...super.props,
]; ];

View File

@@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class ServerAddressFormField extends StatefulWidget { class ServerAddressFormField extends StatefulWidget {
@@ -42,15 +42,14 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
controller: _textEditingController, controller: _textEditingController,
name: ServerAddressFormField.fkServerAddress, name: ServerAddressFormField.fkServerAddress,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
validator: FormBuilderValidators.compose([ validator: (value) {
FormBuilderValidators.required( if (value?.trim().isEmpty ?? true) {
errorText: S.of(context)!.serverAddressMustNotBeEmpty, return S.of(context)!.serverAddressMustNotBeEmpty;
), }
FormBuilderValidators.match( if (!RegExp(r"https?://.*").hasMatch(value!)) {
r"https?://.*", return S.of(context)!.serverAddressMustIncludeAScheme;
errorText: S.of(context)!.serverAddressMustIncludeAScheme, }
) },
]),
decoration: InputDecoration( decoration: InputDecoration(
hintText: "http://192.168.1.50:8000", hintText: "http://192.168.1.50:8000",
labelText: S.of(context)!.serverAddress, labelText: S.of(context)!.serverAddress,

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart'; import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
@@ -36,9 +36,11 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
field.value?.copyWith(username: username) ?? field.value?.copyWith(username: username) ??
UserCredentials(username: username), UserCredentials(username: username),
), ),
validator: FormBuilderValidators.required( validator: (value) {
errorText: S.of(context)!.usernameMustNotBeEmpty, if (value?.trim().isEmpty ?? true) {
), return S.of(context)!.usernameMustNotBeEmpty;
}
},
autofillHints: const [AutofillHints.username], autofillHints: const [AutofillHints.username],
decoration: InputDecoration( decoration: InputDecoration(
label: Text(S.of(context)!.username), label: Text(S.of(context)!.username),
@@ -51,9 +53,11 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
field.value?.copyWith(password: password) ?? field.value?.copyWith(password: password) ??
UserCredentials(password: password), UserCredentials(password: password),
), ),
validator: FormBuilderValidators.required( validator: (value) {
errorText: S.of(context)!.passwordMustNotBeEmpty, if (value?.trim().isEmpty ?? true) {
), return S.of(context)!.passwordMustNotBeEmpty;
}
},
), ),
].map((child) => child.padded()).toList(), ].map((child) => child.padded()).toList(),
), ),

View File

@@ -15,12 +15,14 @@ class SavedViewCubit extends Cubit<SavedViewState> {
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
SavedViewCubit(this._savedViewRepository, this._labelRepository) SavedViewCubit(this._savedViewRepository, this._labelRepository)
: super(SavedViewState.initial( : super(
correspondents: _labelRepository.state.correspondents, SavedViewState.initial(
documentTypes: _labelRepository.state.documentTypes, correspondents: _labelRepository.state.correspondents,
storagePaths: _labelRepository.state.storagePaths, documentTypes: _labelRepository.state.documentTypes,
tags: _labelRepository.state.tags, storagePaths: _labelRepository.state.storagePaths,
)) { tags: _labelRepository.state.tags,
),
) {
_labelRepository.addListener( _labelRepository.addListener(
this, this,
onChanged: (labels) { onChanged: (labels) {

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
@@ -55,7 +55,12 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
children: [ children: [
FormBuilderTextField( FormBuilderTextField(
name: _AddSavedViewPageState.fkName, name: _AddSavedViewPageState.fkName,
validator: FormBuilderValidators.required(), validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
}
return null;
},
decoration: InputDecoration( decoration: InputDecoration(
label: Text(S.of(context)!.name), label: Text(S.of(context)!.name),
), ),

View File

@@ -23,8 +23,13 @@ class SavedViewList extends StatelessWidget {
Center( Center(
child: Text("Saved views loading..."), child: Text("Saved views loading..."),
), ),
loaded: (savedViews, correspondents, documentTypes, tags, loaded: (
storagePaths) { savedViews,
correspondents,
documentTypes,
tags,
storagePaths,
) {
if (savedViews.isEmpty) { if (savedViews.isEmpty) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: HintCard( child: HintCard(
@@ -47,11 +52,12 @@ class SavedViewList extends StatelessWidget {
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MultiBlocProvider( builder: (ctxt) => MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => SavedViewDetailsCubit( create: (context) => SavedViewDetailsCubit(
context.read(), ctxt.read(),
ctxt.read(),
context.read(), context.read(),
savedView: view, savedView: view,
), ),
@@ -71,7 +77,12 @@ class SavedViewList extends StatelessWidget {
), ),
); );
}, },
error: (correspondents, documentTypes, tags, storagePaths) => error: (
correspondents,
documentTypes,
tags,
storagePaths,
) =>
Center( Center(
child: Text( child: Text(
"An error occurred while trying to load the saved views.", "An error occurred while trying to load the saved views.",

View File

@@ -2,6 +2,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -14,20 +15,42 @@ class SavedViewDetailsCubit extends HydratedCubit<SavedViewDetailsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override @override
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
final SavedView savedView; final SavedView savedView;
SavedViewDetailsCubit( SavedViewDetailsCubit(
this.api, this.api,
this.notifier, { this.notifier,
this._labelRepository, {
required this.savedView, required this.savedView,
}) : super(const SavedViewDetailsState()) { }) : super(
SavedViewDetailsState(
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
tags: _labelRepository.state.tags,
storagePaths: _labelRepository.state.storagePaths,
),
) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
onUpdated: replace, onUpdated: replace,
); );
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
));
},
);
updateFilter(filter: savedView.toDocumentFilter()); updateFilter(filter: savedView.toDocumentFilter());
} }

View File

@@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
as s;
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
typedef OpenSearchCallback = void Function(BuildContext context); typedef OpenSearchCallback = void Function(BuildContext context);
class SearchAppBar extends StatefulWidget with PreferredSizeWidget { class SearchAppBar extends StatefulWidget implements PreferredSizeWidget {
final PreferredSizeWidget? bottom; final PreferredSizeWidget? bottom;
final OpenSearchCallback onOpenSearch; final OpenSearchCallback onOpenSearch;
final Color? backgroundColor; final Color? backgroundColor;
@@ -37,7 +38,7 @@ class _SearchAppBarState extends State<SearchAppBar> {
snap: true, snap: true,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
title: SearchBar( title: s.SearchBar(
height: kToolbarHeight - 12, height: kToolbarHeight - 12,
supportingText: widget.hintText, supportingText: widget.hintText,
onTap: () => widget.onOpenSearch(context), onTap: () => widget.onOpenSearch(context),

View File

@@ -693,5 +693,9 @@
"donateCoffee": "Buy me a coffee", "donateCoffee": "Buy me a coffee",
"@donateCoffee": { "@donateCoffee": {
"description": "Label displayed in the app drawer" "description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
} }
} }

View File

@@ -693,5 +693,9 @@
"donateCoffee": "Spendiere mir einen Kaffee", "donateCoffee": "Spendiere mir einen Kaffee",
"@donateCoffee": { "@donateCoffee": {
"description": "Label displayed in the app drawer" "description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
} }
} }

View File

@@ -693,5 +693,9 @@
"donateCoffee": "Buy me a coffee", "donateCoffee": "Buy me a coffee",
"@donateCoffee": { "@donateCoffee": {
"description": "Label displayed in the app drawer" "description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
} }
} }

View File

@@ -693,5 +693,9 @@
"donateCoffee": "Buy me a coffee", "donateCoffee": "Buy me a coffee",
"@donateCoffee": { "@donateCoffee": {
"description": "Label displayed in the app drawer" "description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
} }
} }

View File

@@ -693,5 +693,9 @@
"donateCoffee": "Buy me a coffee", "donateCoffee": "Buy me a coffee",
"@donateCoffee": { "@donateCoffee": {
"description": "Label displayed in the app drawer" "description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
} }
} }

701
lib/l10n/intl_ru.arb Normal file
View File

@@ -0,0 +1,701 @@
{
"developedBy": "Разработано {name}",
"@developedBy": {
"placeholders": {
"name": {}
}
},
"addAnotherAccount": "Добавить другую учетную запись",
"@addAnotherAccount": {},
"account": "Учётная запись",
"@account": {},
"addCorrespondent": "Новый корреспондент",
"@addCorrespondent": {
"description": "Title when adding a new correspondent"
},
"addDocumentType": "Новый тип документа",
"@addDocumentType": {
"description": "Title when adding a new document type"
},
"addStoragePath": "Новый путь хранения",
"@addStoragePath": {
"description": "Title when adding a new storage path"
},
"addTag": "New Tag",
"@addTag": {
"description": "Title when adding a new tag"
},
"aboutThisApp": "About this app",
"@aboutThisApp": {
"description": "Label for about this app tile displayed in the drawer"
},
"loggedInAs": "Logged in as {name}",
"@loggedInAs": {
"placeholders": {
"name": {}
}
},
"disconnect": "Disconnect",
"@disconnect": {
"description": "Logout button label"
},
"reportABug": "Report a Bug",
"@reportABug": {},
"settings": "Settings",
"@settings": {},
"authenticateOnAppStart": "Authenticate on app start",
"@authenticateOnAppStart": {
"description": "Description of the biometric authentication settings tile"
},
"biometricAuthentication": "Biometric authentication",
"@biometricAuthentication": {},
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}",
"@authenticateToToggleBiometricAuthentication": {
"placeholders": {
"mode": {}
}
},
"documents": "Documents",
"@documents": {},
"inbox": "Inbox",
"@inbox": {},
"labels": "Labels",
"@labels": {},
"scanner": "Scanner",
"@scanner": {},
"startTyping": "Start typing...",
"@startTyping": {},
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Delete view ",
"@deleteView": {},
"addedAt": "Added at",
"@addedAt": {},
"archiveSerialNumber": "Archive Serial Number",
"@archiveSerialNumber": {},
"asn": "ASN",
"@asn": {},
"correspondent": "Correspondent",
"@correspondent": {},
"createdAt": "Created at",
"@createdAt": {},
"documentSuccessfullyDeleted": "Document successfully deleted.",
"@documentSuccessfullyDeleted": {},
"assignAsn": "Assign ASN",
"@assignAsn": {},
"deleteDocumentTooltip": "Delete",
"@deleteDocumentTooltip": {
"description": "Tooltip shown for the delete button on details page"
},
"downloadDocumentTooltip": "Download",
"@downloadDocumentTooltip": {
"description": "Tooltip shown for the download button on details page"
},
"editDocumentTooltip": "Edit",
"@editDocumentTooltip": {
"description": "Tooltip shown for the edit button on details page"
},
"loadFullContent": "Load full content",
"@loadFullContent": {},
"noAppToDisplayPDFFilesFound": "No app to display PDF files found!",
"@noAppToDisplayPDFFilesFound": {},
"openInSystemViewer": "Open in system viewer",
"@openInSystemViewer": {},
"couldNotOpenFilePermissionDenied": "Could not open file: Permission denied.",
"@couldNotOpenFilePermissionDenied": {},
"previewTooltip": "Preview",
"@previewTooltip": {
"description": "Tooltip shown for the preview button on details page"
},
"shareTooltip": "Share",
"@shareTooltip": {
"description": "Tooltip shown for the share button on details page"
},
"similarDocuments": "Similar Documents",
"@similarDocuments": {
"description": "Label shown in the tabbar on details page"
},
"content": "Content",
"@content": {
"description": "Label shown in the tabbar on details page"
},
"metaData": "Meta Data",
"@metaData": {
"description": "Label shown in the tabbar on details page"
},
"overview": "Overview",
"@overview": {
"description": "Label shown in the tabbar on details page"
},
"documentType": "Document Type",
"@documentType": {},
"archivedPdf": "Archived (pdf)",
"@archivedPdf": {
"description": "Option to chose when downloading a document"
},
"chooseFiletype": "Choose filetype",
"@chooseFiletype": {},
"original": "Original",
"@original": {
"description": "Option to chose when downloading a document"
},
"documentSuccessfullyDownloaded": "Document successfully downloaded.",
"@documentSuccessfullyDownloaded": {},
"suggestions": "Suggestions: ",
"@suggestions": {},
"editDocument": "Edit Document",
"@editDocument": {},
"advanced": "Advanced",
"@advanced": {},
"apply": "Apply",
"@apply": {},
"extended": "Extended",
"@extended": {},
"titleAndContent": "Title & Content",
"@titleAndContent": {},
"title": "Title",
"@title": {},
"reset": "Reset",
"@reset": {},
"filterDocuments": "Filter Documents",
"@filterDocuments": {
"description": "Title of the document filter"
},
"originalMD5Checksum": "Original MD5-Checksum",
"@originalMD5Checksum": {},
"mediaFilename": "Media Filename",
"@mediaFilename": {},
"originalFileSize": "Original File Size",
"@originalFileSize": {},
"originalMIMEType": "Original MIME-Type",
"@originalMIMEType": {},
"modifiedAt": "Modified at",
"@modifiedAt": {},
"preview": "Preview",
"@preview": {
"description": "Title of the document preview page"
},
"scanADocument": "Scan a document",
"@scanADocument": {},
"noDocumentsScannedYet": "No documents scanned yet.",
"@noDocumentsScannedYet": {},
"or": "or",
"@or": {
"description": "Used on the scanner page between both main actions when no scans have been captured."
},
"deleteAllScans": "Delete all scans",
"@deleteAllScans": {},
"uploadADocumentFromThisDevice": "Upload a document from this device",
"@uploadADocumentFromThisDevice": {
"description": "Button label on scanner page"
},
"noMatchesFound": "No matches found.",
"@noMatchesFound": {
"description": "Displayed when no documents were found in the document search."
},
"removeFromSearchHistory": "Remove from search history?",
"@removeFromSearchHistory": {},
"results": "Results",
"@results": {
"description": "Label displayed above search results in document search."
},
"searchDocuments": "Search documents",
"@searchDocuments": {},
"resetFilter": "Reset filter",
"@resetFilter": {},
"lastMonth": "Last Month",
"@lastMonth": {},
"last7Days": "Last 7 Days",
"@last7Days": {},
"last3Months": "Last 3 Months",
"@last3Months": {},
"lastYear": "Last Year",
"@lastYear": {},
"search": "Search",
"@search": {},
"documentsSuccessfullyDeleted": "Documents successfully deleted.",
"@documentsSuccessfullyDeleted": {},
"thereSeemsToBeNothingHere": "There seems to be nothing here...",
"@thereSeemsToBeNothingHere": {},
"oops": "Oops.",
"@oops": {},
"newDocumentAvailable": "New document available!",
"@newDocumentAvailable": {},
"orderBy": "Order By",
"@orderBy": {},
"thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?",
"@thisActionIsIrreversibleDoYouWishToProceedAnyway": {},
"confirmDeletion": "Confirm deletion",
"@confirmDeletion": {},
"areYouSureYouWantToDeleteTheFollowingDocuments": "{count, plural, one{Are you sure you want to delete the following document?} other{Are you sure you want to delete the following documents?}}",
"@areYouSureYouWantToDeleteTheFollowingDocuments": {
"placeholders": {
"count": {}
}
},
"countSelected": "{count} selected",
"@countSelected": {
"description": "Displayed in the appbar when at least one document is selected.",
"placeholders": {
"count": {}
}
},
"storagePath": "Storage Path",
"@storagePath": {},
"prepareDocument": "Prepare document",
"@prepareDocument": {},
"tags": "Tags",
"@tags": {},
"documentSuccessfullyUpdated": "Document successfully updated.",
"@documentSuccessfullyUpdated": {},
"fileName": "File Name",
"@fileName": {},
"synchronizeTitleAndFilename": "Synchronize title and filename",
"@synchronizeTitleAndFilename": {},
"reload": "Reload",
"@reload": {},
"documentSuccessfullyUploadedProcessing": "Document successfully uploaded, processing...",
"@documentSuccessfullyUploadedProcessing": {},
"deleteLabelWarningText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
"@deleteLabelWarningText": {},
"couldNotAcknowledgeTasks": "Could not acknowledge tasks.",
"@couldNotAcknowledgeTasks": {},
"authenticationFailedPleaseTryAgain": "Authentication failed, please try again.",
"@authenticationFailedPleaseTryAgain": {},
"anErrorOccurredWhileTryingToAutocompleteYourQuery": "An error ocurred while trying to autocomplete your query.",
"@anErrorOccurredWhileTryingToAutocompleteYourQuery": {},
"biometricAuthenticationFailed": "Biometric authentication failed.",
"@biometricAuthenticationFailed": {},
"biometricAuthenticationNotSupported": "Biometric authentication not supported on this device.",
"@biometricAuthenticationNotSupported": {},
"couldNotBulkEditDocuments": "Could not bulk edit documents.",
"@couldNotBulkEditDocuments": {},
"couldNotCreateCorrespondent": "Could not create correspondent, please try again.",
"@couldNotCreateCorrespondent": {},
"couldNotLoadCorrespondents": "Could not load correspondents.",
"@couldNotLoadCorrespondents": {},
"couldNotCreateSavedView": "Could not create saved view, please try again.",
"@couldNotCreateSavedView": {},
"couldNotDeleteSavedView": "Could not delete saved view, please try again",
"@couldNotDeleteSavedView": {},
"youAreCurrentlyOffline": "You are currently offline. Please make sure you are connected to the internet.",
"@youAreCurrentlyOffline": {},
"couldNotAssignArchiveSerialNumber": "Could not assign archive serial number.",
"@couldNotAssignArchiveSerialNumber": {},
"couldNotDeleteDocument": "Could not delete document, please try again.",
"@couldNotDeleteDocument": {},
"couldNotLoadDocuments": "Could not load documents, please try again.",
"@couldNotLoadDocuments": {},
"couldNotLoadDocumentPreview": "Could not load document preview.",
"@couldNotLoadDocumentPreview": {},
"couldNotCreateDocument": "Could not create document, please try again.",
"@couldNotCreateDocument": {},
"couldNotLoadDocumentTypes": "Could not load document types, please try again.",
"@couldNotLoadDocumentTypes": {},
"couldNotUpdateDocument": "Could not update document, please try again.",
"@couldNotUpdateDocument": {},
"couldNotUploadDocument": "Could not upload document, please try again.",
"@couldNotUploadDocument": {},
"invalidCertificateOrMissingPassphrase": "Invalid certificate or missing passphrase, please try again",
"@invalidCertificateOrMissingPassphrase": {},
"couldNotLoadSavedViews": "Could not load saved views.",
"@couldNotLoadSavedViews": {},
"aClientCertificateWasExpectedButNotSent": "A client certificate was expected but not sent. Please provide a valid client certificate.",
"@aClientCertificateWasExpectedButNotSent": {},
"userIsNotAuthenticated": "User is not authenticated.",
"@userIsNotAuthenticated": {},
"requestTimedOut": "The request to the server timed out.",
"@requestTimedOut": {},
"anErrorOccurredRemovingTheScans": "An error occurred removing the scans.",
"@anErrorOccurredRemovingTheScans": {},
"couldNotReachYourPaperlessServer": "Could not reach your Paperless server, is it up and running?",
"@couldNotReachYourPaperlessServer": {},
"couldNotLoadSimilarDocuments": "Could not load similar documents.",
"@couldNotLoadSimilarDocuments": {},
"couldNotCreateStoragePath": "Could not create storage path, please try again.",
"@couldNotCreateStoragePath": {},
"couldNotLoadStoragePaths": "Could not load storage paths.",
"@couldNotLoadStoragePaths": {},
"couldNotLoadSuggestions": "Could not load suggestions.",
"@couldNotLoadSuggestions": {},
"couldNotCreateTag": "Could not create tag, please try again.",
"@couldNotCreateTag": {},
"couldNotLoadTags": "Could not load tags.",
"@couldNotLoadTags": {},
"anUnknownErrorOccurred": "An unknown error occurred.",
"@anUnknownErrorOccurred": {},
"fileFormatNotSupported": "This file format is not supported.",
"@fileFormatNotSupported": {},
"report": "REPORT",
"@report": {},
"absolute": "Absolute",
"@absolute": {},
"hintYouCanAlsoSpecifyRelativeValues": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.",
"@hintYouCanAlsoSpecifyRelativeValues": {
"description": "Displayed in the extended date range picker"
},
"amount": "Amount",
"@amount": {},
"relative": "Relative",
"@relative": {},
"last": "Last",
"@last": {},
"timeUnit": "Time unit",
"@timeUnit": {},
"selectDateRange": "Select date range",
"@selectDateRange": {},
"after": "After",
"@after": {},
"before": "Before",
"@before": {},
"days": "{count, plural, zero{days} one{day} other{days}}",
"@days": {
"placeholders": {
"count": {}
}
},
"lastNDays": "{count, plural, zero{} one{Yesterday} other{Last {count} days}}",
"@lastNDays": {
"placeholders": {
"count": {}
}
},
"lastNMonths": "{count, plural, zero{} one{Last month} other{Last {count} months}}",
"@lastNMonths": {
"placeholders": {
"count": {}
}
},
"lastNWeeks": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}",
"@lastNWeeks": {
"placeholders": {
"count": {}
}
},
"lastNYears": "{count, plural, zero{} one{Last year} other{Last {count} years}}",
"@lastNYears": {
"placeholders": {
"count": {}
}
},
"months": "{count, plural, zero{} one{month} other{months}}",
"@months": {
"placeholders": {
"count": {}
}
},
"weeks": "{count, plural, zero{} one{week} other{weeks}}",
"@weeks": {
"placeholders": {
"count": {}
}
},
"years": "{count, plural, zero{} one{year} other{years}}",
"@years": {
"placeholders": {
"count": {}
}
},
"gotIt": "Got it!",
"@gotIt": {},
"cancel": "Cancel",
"@cancel": {},
"close": "Close",
"@close": {},
"create": "Create",
"@create": {},
"delete": "Delete",
"@delete": {},
"edit": "Edit",
"@edit": {},
"ok": "Ok",
"@ok": {},
"save": "Save",
"@save": {},
"select": "Select",
"@select": {},
"saveChanges": "Save changes",
"@saveChanges": {},
"upload": "Upload",
"@upload": {},
"youreOffline": "You're offline.",
"@youreOffline": {},
"deleteDocument": "Delete document",
"@deleteDocument": {
"description": "Used as an action label on each inbox item"
},
"removeDocumentFromInbox": "Document removed from inbox.",
"@removeDocumentFromInbox": {},
"areYouSureYouWantToMarkAllDocumentsAsSeen": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents. This action is not reversible! Are you sure you want to continue?",
"@areYouSureYouWantToMarkAllDocumentsAsSeen": {},
"markAllAsSeen": "Mark all as seen?",
"@markAllAsSeen": {},
"allSeen": "All seen",
"@allSeen": {},
"markAsSeen": "Mark as seen",
"@markAsSeen": {},
"refresh": "Refresh",
"@refresh": {},
"youDoNotHaveUnseenDocuments": "You do not have unseen documents.",
"@youDoNotHaveUnseenDocuments": {},
"quickAction": "Quick Action",
"@quickAction": {},
"suggestionSuccessfullyApplied": "Suggestion successfully applied.",
"@suggestionSuccessfullyApplied": {},
"today": "Today",
"@today": {},
"undo": "Undo",
"@undo": {},
"nUnseen": "{count} unseen",
"@nUnseen": {
"placeholders": {
"count": {}
}
},
"swipeLeftToMarkADocumentAsSeen": "Hint: Swipe left to mark a document as seen and remove all inbox tags from the document.",
"@swipeLeftToMarkADocumentAsSeen": {},
"yesterday": "Yesterday",
"@yesterday": {},
"anyAssigned": "Any assigned",
"@anyAssigned": {},
"noItemsFound": "No items found!",
"@noItemsFound": {},
"caseIrrelevant": "Case Irrelevant",
"@caseIrrelevant": {},
"matchingAlgorithm": "Matching Algorithm",
"@matchingAlgorithm": {},
"match": "Match",
"@match": {},
"name": "Name",
"@name": {},
"notAssigned": "Not assigned",
"@notAssigned": {},
"addNewCorrespondent": "Add new correspondent",
"@addNewCorrespondent": {},
"noCorrespondentsSetUp": "You don't seem to have any correspondents set up.",
"@noCorrespondentsSetUp": {},
"correspondents": "Correspondents",
"@correspondents": {},
"addNewDocumentType": "Add new document type",
"@addNewDocumentType": {},
"noDocumentTypesSetUp": "You don't seem to have any document types set up.",
"@noDocumentTypesSetUp": {},
"documentTypes": "Document Types",
"@documentTypes": {},
"addNewStoragePath": "Add new storage path",
"@addNewStoragePath": {},
"noStoragePathsSetUp": "You don't seem to have any storage paths set up.",
"@noStoragePathsSetUp": {},
"storagePaths": "Storage Paths",
"@storagePaths": {},
"addNewTag": "Add new tag",
"@addNewTag": {},
"noTagsSetUp": "You don't seem to have any tags set up.",
"@noTagsSetUp": {},
"linkedDocuments": "Linked Documents",
"@linkedDocuments": {},
"advancedSettings": "Advanced Settings",
"@advancedSettings": {},
"passphrase": "Passphrase",
"@passphrase": {},
"configureMutualTLSAuthentication": "Configure Mutual TLS Authentication",
"@configureMutualTLSAuthentication": {},
"invalidCertificateFormat": "Invalid certificate format, only .pfx is allowed",
"@invalidCertificateFormat": {},
"clientcertificate": "Client Certificate",
"@clientcertificate": {},
"selectFile": "Select file...",
"@selectFile": {},
"continueLabel": "Continue",
"@continueLabel": {},
"incorrectOrMissingCertificatePassphrase": "Incorrect or missing certificate passphrase.",
"@incorrectOrMissingCertificatePassphrase": {},
"connect": "Connect",
"@connect": {},
"password": "Password",
"@password": {},
"passwordMustNotBeEmpty": "Password must not be empty.",
"@passwordMustNotBeEmpty": {},
"connectionTimedOut": "Connection timed out.",
"@connectionTimedOut": {},
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
"@loginPageReachabilityMissingClientCertificateText": {},
"couldNotEstablishConnectionToTheServer": "Could not establish a connection to the server.",
"@couldNotEstablishConnectionToTheServer": {},
"connectionSuccessfulylEstablished": "Connection successfully established.",
"@connectionSuccessfulylEstablished": {},
"hostCouldNotBeResolved": "Host could not be resolved. Please check the server address and your internet connection. ",
"@hostCouldNotBeResolved": {},
"serverAddress": "Server Address",
"@serverAddress": {},
"invalidAddress": "Invalid address.",
"@invalidAddress": {},
"serverAddressMustIncludeAScheme": "Server address must include a scheme.",
"@serverAddressMustIncludeAScheme": {},
"serverAddressMustNotBeEmpty": "Server address must not be empty.",
"@serverAddressMustNotBeEmpty": {},
"signIn": "Sign In",
"@signIn": {},
"loginPageSignInTitle": "Sign In",
"@loginPageSignInTitle": {},
"signInToServer": "Sign in to {serverAddress}",
"@signInToServer": {
"placeholders": {
"serverAddress": {}
}
},
"connectToPaperless": "Connect to Paperless",
"@connectToPaperless": {},
"username": "Username",
"@username": {},
"usernameMustNotBeEmpty": "Username must not be empty.",
"@usernameMustNotBeEmpty": {},
"documentContainsAllOfTheseWords": "Document contains all of these words",
"@documentContainsAllOfTheseWords": {},
"all": "All",
"@all": {},
"documentContainsAnyOfTheseWords": "Document contains any of these words",
"@documentContainsAnyOfTheseWords": {},
"any": "Any",
"@any": {},
"learnMatchingAutomatically": "Learn matching automatically",
"@learnMatchingAutomatically": {},
"auto": "Auto",
"@auto": {},
"documentContainsThisString": "Document contains this string",
"@documentContainsThisString": {},
"exact": "Exact",
"@exact": {},
"documentContainsAWordSimilarToThisWord": "Document contains a word similar to this word",
"@documentContainsAWordSimilarToThisWord": {},
"fuzzy": "Fuzzy",
"@fuzzy": {},
"documentMatchesThisRegularExpression": "Document matches this regular expression",
"@documentMatchesThisRegularExpression": {},
"regularExpression": "Regular Expression",
"@regularExpression": {},
"anInternetConnectionCouldNotBeEstablished": "An internet connection could not be established.",
"@anInternetConnectionCouldNotBeEstablished": {},
"done": "Done",
"@done": {},
"next": "Next",
"@next": {},
"couldNotAccessReceivedFile": "Could not access the received file. Please try to open the app before sharing.",
"@couldNotAccessReceivedFile": {},
"newView": "New View",
"@newView": {},
"createsASavedViewBasedOnTheCurrentFilterCriteria": "Creates a new view based on the current filter criteria.",
"@createsASavedViewBasedOnTheCurrentFilterCriteria": {},
"createViewsToQuicklyFilterYourDocuments": "Create views to quickly filter your documents.",
"@createViewsToQuicklyFilterYourDocuments": {},
"nFiltersSet": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}",
"@nFiltersSet": {
"placeholders": {
"count": {}
}
},
"showInSidebar": "Show in sidebar",
"@showInSidebar": {},
"showOnDashboard": "Show on dashboard",
"@showOnDashboard": {},
"views": "Views",
"@views": {},
"clearAll": "Clear all",
"@clearAll": {},
"scan": "Scan",
"@scan": {},
"previewScan": "Preview",
"@previewScan": {},
"scrollToTop": "Scroll to top",
"@scrollToTop": {},
"paperlessServerVersion": "Paperless server version",
"@paperlessServerVersion": {},
"darkTheme": "Dark Theme",
"@darkTheme": {},
"lightTheme": "Light Theme",
"@lightTheme": {},
"systemTheme": "Use system theme",
"@systemTheme": {},
"appearance": "Appearance",
"@appearance": {},
"languageAndVisualAppearance": "Language and visual appearance",
"@languageAndVisualAppearance": {},
"applicationSettings": "Application",
"@applicationSettings": {},
"colorSchemeHint": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.",
"@colorSchemeHint": {},
"colorSchemeNotSupportedWarning": "Dynamic theming is only supported for devices running Android 12 and above. Selecting the 'Dynamic' option might not have any effect depending on your OS implementation.",
"@colorSchemeNotSupportedWarning": {},
"colors": "Colors",
"@colors": {},
"language": "Language",
"@language": {},
"security": "Security",
"@security": {},
"mangeFilesAndStorageSpace": "Manage files and storage space",
"@mangeFilesAndStorageSpace": {},
"storage": "Storage",
"@storage": {},
"dark": "Dark",
"@dark": {},
"light": "Light",
"@light": {},
"system": "System",
"@system": {},
"ascending": "Ascending",
"@ascending": {},
"descending": "Descending",
"@descending": {},
"storagePathDay": "day",
"@storagePathDay": {},
"storagePathMonth": "month",
"@storagePathMonth": {},
"storagePathYear": "year",
"@storagePathYear": {},
"color": "Color",
"@color": {},
"filterTags": "Filter tags...",
"@filterTags": {},
"inboxTag": "Inbox-Tag",
"@inboxTag": {},
"uploadInferValuesHint": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.",
"@uploadInferValuesHint": {},
"useTheConfiguredBiometricFactorToAuthenticate": "Use the configured biometric factor to authenticate and unlock your documents.",
"@useTheConfiguredBiometricFactorToAuthenticate": {},
"verifyYourIdentity": "Verify your identity",
"@verifyYourIdentity": {},
"verifyIdentity": "Verify Identity",
"@verifyIdentity": {},
"detailed": "Detailed",
"@detailed": {},
"grid": "Grid",
"@grid": {},
"list": "List",
"@list": {},
"remove": "Remove",
"removeQueryFromSearchHistory": "Remove query from search history?",
"dynamicColorScheme": "Dynamic",
"@dynamicColorScheme": {},
"classicColorScheme": "Classic",
"@classicColorScheme": {},
"notificationDownloadComplete": "Download complete",
"@notificationDownloadComplete": {
"description": "Notification title when a download has been completed."
},
"notificationDownloadingDocument": "Downloading document",
"@notificationDownloadingDocument": {
"description": "Notification title shown when a document download is pending"
},
"archiveSerialNumberUpdated": "Archive Serial Number updated.",
"@archiveSerialNumberUpdated": {
"description": "Message shown when the ASN has been updated."
},
"donateCoffee": "Buy me a coffee",
"@donateCoffee": {
"description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
}
}

View File

@@ -693,5 +693,9 @@
"donateCoffee": "Buy me a coffee", "donateCoffee": "Buy me a coffee",
"@donateCoffee": { "@donateCoffee": {
"description": "Label displayed in the app drawer" "description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "This field is required!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
} }
} }

View File

@@ -8,7 +8,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_standalone.dart'; import 'package:intl/intl_standalone.dart';
@@ -223,7 +223,6 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
], ],
routes: { routes: {
DocumentDetailsRoute.routeName: (context) => DocumentDetailsRoute.routeName: (context) =>

View File

@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
@@ -22,7 +23,7 @@ class DocumentDetailsRoute extends StatelessWidget {
initialDocument: args.document, initialDocument: args.document,
), ),
child: RepositoryProvider.value( child: RepositoryProvider.value(
value: context.read(), value: context.read<LabelRepository>(),
child: DocumentDetailsPage( child: DocumentDetailsPage(
allowEdit: args.allowEdit, allowEdit: args.allowEdit,
isLabelClickable: args.isLabelClickable, isLabelClickable: args.isLabelClickable,

View File

@@ -1,4 +1,4 @@
import 'dart:ui'; import 'package:flutter/painting.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';

View File

@@ -16,7 +16,7 @@ dependencies:
equatable: ^2.0.5 equatable: ^2.0.5
http: ^0.13.5 http: ^0.13.5
json_annotation: ^4.7.0 json_annotation: ^4.7.0
intl: ^0.17.0 intl: any #^0.18.0
dio: ^5.0.0 dio: ^5.0.0
collection: ^1.17.0 collection: ^1.17.0
jiffy: ^5.0.0 jiffy: ^5.0.0

View File

@@ -58,7 +58,8 @@ class _AnimatedTouchBubblePartState extends State<AnimatedTouchBubblePart>
width: widget.dragging ? 0 : widget.size / 2, width: widget.dragging ? 0 : widget.size / 2,
height: widget.dragging ? 0 : widget.size / 2, height: widget.dragging ? 0 : widget.size / 2,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).accentColor.withOpacity(0.5), color:
Theme.of(context).colorScheme.primary.withOpacity(0.5),
borderRadius: widget.dragging borderRadius: widget.dragging
? BorderRadius.circular(widget.size) ? BorderRadius.circular(widget.size)
: BorderRadius.circular(widget.size / 4)))), : BorderRadius.circular(widget.size / 4)))),

View File

@@ -78,7 +78,8 @@ class _MagnifierState extends State<Magnifier> {
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.matrix(_matrix.storage), filter: ImageFilter.matrix(_matrix.storage),
child: CustomPaint( child: CustomPaint(
painter: MagnifierPainter(color: Theme.of(context).accentColor), painter: MagnifierPainter(
color: Theme.of(context).colorScheme.secondary),
size: _magnifierSize, size: _magnifierSize,
), ),
), ),

View File

@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.2" version: "0.11.2"
animations:
dependency: "direct main"
description:
name: animations
sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164
url: "https://pub.dev"
source: hosted
version: "2.0.7"
ansicolor: ansicolor:
dependency: transitive dependency: transitive
description: description:
@@ -560,10 +568,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_form_builder name: flutter_form_builder
sha256: "768b11307e71c60cb66351a87984815bd438b50aa58b5de02c9256a8f2964bee" sha256: "9551c7379adc01a3a3a1100057396407c9534ea8adc937d14a0edd96fcd9e1dc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.7.0" version: "7.8.0"
flutter_html: flutter_html:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -723,14 +731,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.4.0" version: "10.4.0"
form_builder_validators:
dependency: "direct main"
description:
name: form_builder_validators
sha256: e4d54c0c513e3e36ae4e4905994873a0a907585407212effeef39a68e759670c
url: "https://pub.dev"
source: hosted
version: "8.4.0"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -1009,10 +1009,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: mockito name: mockito
sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.2" version: "5.4.0"
mocktail: mocktail:
dependency: transitive dependency: transitive
description: description:
@@ -1842,5 +1842,5 @@ packages:
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=2.19.0 <3.0.0" dart: ">2.19.0 <3.0.0"
flutter: ">=3.4.0-17.0.pre" flutter: ">=3.7.0"

View File

@@ -44,7 +44,7 @@ dependencies:
path_provider: ^2.0.10 path_provider: ^2.0.10
image: ^3.1.3 image: ^3.1.3
photo_view: ^0.14.0 photo_view: ^0.14.0
intl: ^0.17.0 intl: any #^0.18.0
flutter_svg: ^1.0.3 flutter_svg: ^1.0.3
url_launcher: ^6.1.2 url_launcher: ^6.1.2
file_picker: ^5.2.4 file_picker: ^5.2.4
@@ -57,7 +57,7 @@ dependencies:
flutter_bloc: ^8.1.1 flutter_bloc: ^8.1.1
equatable: ^2.0.3 equatable: ^2.0.3
flutter_form_builder: ^7.5.0 flutter_form_builder: ^7.5.0
form_builder_validators: ^8.4.0 #form_builder_validators: ^8.4.0
package_info_plus: ^1.4.3+1 package_info_plus: ^1.4.3+1
font_awesome_flutter: ^10.1.0 font_awesome_flutter: ^10.1.0
local_auth: ^2.1.2 local_auth: ^2.1.2
@@ -92,6 +92,7 @@ dependencies:
flutter_html: ^3.0.0-alpha.6 flutter_html: ^3.0.0-alpha.6
in_app_review: ^2.0.6 in_app_review: ^2.0.6
freezed_annotation: ^2.2.0 freezed_annotation: ^2.2.0
animations: ^2.0.7
dev_dependencies: dev_dependencies:
integration_test: integration_test: