mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 12:07:58 -06:00
feat: Replaced old label form fields with full page search, removed badge from edit button in document details
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
1885
lib/core/widgets/material/search/search_anchor.dart
Normal file
1885
lib/core/widgets/material/search/search_anchor.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
264
lib/features/labels/view/widgets/fullscreen_label_form.dart
Normal file
264
lib/features/labels/view/widgets/fullscreen_label_form.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
701
lib/l10n/intl_ru.arb
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
28
pubspec.lock
28
pubspec.lock
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user