diff --git a/.gitignore b/.gitignore index 275e0b4..9e5fafc 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,6 @@ lib/generated/* untranslated_messages.txt #lakos generated files -**/dot_images/* \ No newline at end of file +**/dot_images/* + +docker/ \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 39e075e..c7a26c2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -82,10 +82,13 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // Required for flutter_local_notifications coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2572141..c4a3a6a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + @@ -11,10 +12,14 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> - + the + Android process has started. This theme is visible to the user + while + the Flutter UI initializes. After that, this theme continues + to + determine the Window background behind the Flutter UI. --> + @@ -34,14 +39,16 @@ + This is used by the Flutter tool to + generate GeneratedPluginRegistrant.java --> - + android:maxSdkVersion="32" /> + + \ No newline at end of file diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart new file mode 100644 index 0000000..9e5a9b6 --- /dev/null +++ b/lib/extensions/string_extensions.dart @@ -0,0 +1,7 @@ +extension SizeLimitedString on String { + String withLengthLimitedTo(int length, [String overflow = "..."]) { + return this.length > length + ? '${substring(0, length - overflow.length)}$overflow' + : this; + } +} diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index 316b14a..fcc159c 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -35,13 +35,7 @@ class DocumentUploadCubit extends Cubit { _tagRepository = tagRepository, _correspondentRepository = correspondentRepository, _documentTypeRepository = documentTypeRepository, - super( - const DocumentUploadState( - tags: {}, - correspondents: {}, - documentTypes: {}, - ), - ) { + super(const DocumentUploadState()) { _subs.add(_tagRepository.values.listen( (tags) => emit(state.copyWith(tags: tags?.values)), )); diff --git a/lib/features/document_upload/cubit/document_upload_state.dart b/lib/features/document_upload/cubit/document_upload_state.dart index 8a1b78f..61b7fa5 100644 --- a/lib/features/document_upload/cubit/document_upload_state.dart +++ b/lib/features/document_upload/cubit/document_upload_state.dart @@ -7,9 +7,9 @@ class DocumentUploadState extends Equatable { final Map documentTypes; const DocumentUploadState({ - required this.tags, - required this.correspondents, - required this.documentTypes, + this.tags = const {}, + this.correspondents = const {}, + this.documentTypes = const {}, }); @override diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index 4fc8769..d7a22b1 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -236,8 +236,10 @@ class _DocumentUploadPreparationPageState final taskId = await cubit.upload( widget.fileBytes, - filename: - _padWithPdfExtension(_formKey.currentState?.value[fkFileName]), + filename: _padWithExtension( + _formKey.currentState?.value[fkFileName], + widget.fileExtension, + ), title: title, documentType: docType.id, correspondent: correspondent.id, @@ -261,11 +263,12 @@ class _DocumentUploadPreparationPageState } } - String _padWithPdfExtension(String source) { - return source.endsWith(".pdf") ? source : '$source.pdf'; + String _padWithExtension(String source, [String? extension]) { + final ext = extension ?? '.pdf'; + return source.endsWith(ext) ? source : '$source$ext'; } String _formatFilename(String source) { - return source.replaceAll(RegExp(r"[\W_]"), "_"); + return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase(); } } diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 6b489b3..66d7afd 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -219,6 +219,11 @@ class DocumentsCubit extends Cubit with HydratedMixin { } } + Future> autocomplete(String query) async { + final res = await _api.autocomplete(query); + return res; + } + void unselectView() { emit(state.copyWith(selectedSavedViewId: null)); } diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index df2913d..c34fc6b 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -309,7 +309,7 @@ class _DocumentEditPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Suggestions: ", + S.of(context).documentEditPageSuggestionsLabel, style: Theme.of(context).textTheme.bodySmall, ), SizedBox( diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 547f4a1..1f41ad4 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -50,7 +50,7 @@ class _DocumentsPageState extends State { double _offset = 0; double _last = 0; - static const double _savedViewWidgetHeight = 78 + 16; + static const double _savedViewWidgetHeight = 80 + 16; @override void initState() { diff --git a/lib/features/documents/view/widgets/list/document_list_item.dart b/lib/features/documents/view/widgets/list/document_list_item.dart index e73c97b..afe76b8 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/list/document_list_item.dart @@ -36,62 +36,60 @@ class DocumentListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - child: ListTile( - dense: true, - selected: isSelected, - onTap: () => _onTap(), - selectedTileColor: Theme.of(context).colorScheme.inversePrimary, - onLongPress: () => onSelected?.call(document), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Row( - children: [ - AbsorbPointer( - absorbing: isAtLeastOneSelected, - child: CorrespondentWidget( - isClickable: isLabelClickable, - correspondentId: document.correspondent, - onSelected: onCorrespondentSelected, - ), + return ListTile( + dense: true, + selected: isSelected, + onTap: () => _onTap(), + selectedTileColor: Theme.of(context).colorScheme.inversePrimary, + onLongPress: () => onSelected?.call(document), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + children: [ + AbsorbPointer( + absorbing: isAtLeastOneSelected, + child: CorrespondentWidget( + isClickable: isLabelClickable, + correspondentId: document.correspondent, + onSelected: onCorrespondentSelected, ), - ], - ), - Text( - document.title, - overflow: TextOverflow.ellipsis, - maxLines: document.tags.isEmpty ? 2 : 1, - ), - ], - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: AbsorbPointer( - absorbing: isAtLeastOneSelected, - child: TagsWidget( - isClickable: isLabelClickable, - tagIds: document.tags, - isMultiLine: false, - isSelectedPredicate: isTagSelectedPredicate, - onTagSelected: (id) => onTagSelected?.call(id), - ), + ), + ], ), - ), - isThreeLine: document.tags.isNotEmpty, - leading: AspectRatio( - aspectRatio: _a4AspectRatio, - child: GestureDetector( - child: DocumentPreview( - id: document.id, - fit: BoxFit.cover, - alignment: Alignment.topCenter, - ), + Text( + document.title, + overflow: TextOverflow.ellipsis, + maxLines: document.tags.isEmpty ? 2 : 1, ), - ), - contentPadding: const EdgeInsets.all(8.0), + ], ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: AbsorbPointer( + absorbing: isAtLeastOneSelected, + child: TagsWidget( + isClickable: isLabelClickable, + tagIds: document.tags, + isMultiLine: false, + isSelectedPredicate: isTagSelectedPredicate, + onTagSelected: (id) => onTagSelected?.call(id), + ), + ), + ), + isThreeLine: document.tags.isNotEmpty, + leading: AspectRatio( + aspectRatio: _a4AspectRatio, + child: GestureDetector( + child: DocumentPreview( + id: document.id, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + ), + ), + contentPadding: const EdgeInsets.all(8.0), ); } diff --git a/lib/features/documents/view/widgets/search/text_query_form_field.dart b/lib/features/documents/view/widgets/search/text_query_form_field.dart index 420e529..99a90d5 100644 --- a/lib/features/documents/view/widgets/search/text_query_form_field.dart +++ b/lib/features/documents/view/widgets/search/text_query_form_field.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.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/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:provider/provider.dart'; class TextQueryFormField extends StatelessWidget { final String name; @@ -21,57 +24,69 @@ class TextQueryFormField extends StatelessWidget { name: name, initialValue: initialValue, builder: (field) { - return TextFormField( - initialValue: initialValue?.queryText, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search_outlined), - labelText: _buildLabelText(context, field.value!.queryType), - suffixIcon: PopupMenuButton( - icon: onlyExtendedQueryAllowed - ? Icon( - Icons.more_vert, - color: Theme.of(context).disabledColor, - ) - : null, - enabled: !onlyExtendedQueryAllowed, - itemBuilder: (context) => [ - PopupMenuItem( - child: ListTile( - title: Text(S - .of(context) - .documentFilterQueryOptionsTitleAndContentLabel), - ), - value: QueryType.titleAndContent, - ), - PopupMenuItem( - child: ListTile( - title: Text( - S.of(context).documentFilterQueryOptionsTitleLabel), - ), - value: QueryType.title, - ), - PopupMenuItem( - child: ListTile( - title: Text( - S.of(context).documentFilterQueryOptionsExtendedLabel), - ), - value: QueryType.extended, - ), - ], - onSelected: (selection) { - field.didChange(field.value?.copyWith(queryType: selection)); + return Autocomplete( + optionsBuilder: (value) => + context.read().autocomplete(value.text), + initialValue: initialValue?.queryText != null + ? TextEditingValue(text: initialValue!.queryText!) + : null, + fieldViewBuilder: + (context, textEditingController, focusNode, onFieldSubmitted) { + return TextFormField( + controller: textEditingController, + focusNode: focusNode, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search_outlined), + labelText: _buildLabelText(context, field.value!.queryType), + suffixIcon: _buildQueryTypeMenu(context, field), + ), + onChanged: (value) { + field.didChange(field.value?.copyWith(queryText: value)); }, - ), - ), - onChanged: (value) { - field.didChange(field.value?.copyWith(queryText: value)); + ); }, ); }, ); } + PopupMenuButton _buildQueryTypeMenu( + BuildContext context, FormFieldState field) { + return PopupMenuButton( + icon: onlyExtendedQueryAllowed + ? Icon( + Icons.more_vert, + color: Theme.of(context).disabledColor, + ) + : null, + enabled: !onlyExtendedQueryAllowed, + itemBuilder: (context) => [ + PopupMenuItem( + child: ListTile( + title: Text( + S.of(context).documentFilterQueryOptionsTitleAndContentLabel), + ), + value: QueryType.titleAndContent, + ), + PopupMenuItem( + child: ListTile( + title: Text(S.of(context).documentFilterQueryOptionsTitleLabel), + ), + value: QueryType.title, + ), + PopupMenuItem( + child: ListTile( + title: Text(S.of(context).documentFilterQueryOptionsExtendedLabel), + ), + value: QueryType.extended, + ), + ], + onSelected: (selection) { + field.didChange(field.value?.copyWith(queryType: selection)); + }, + ); + } + String _buildLabelText(BuildContext context, QueryType queryType) { switch (queryType) { case QueryType.title: diff --git a/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart b/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart index 90a51ae..ce64f34 100644 --- a/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart +++ b/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class BulkDeleteConfirmationDialog extends StatelessWidget { - static const _bulletPoint = "\u2022"; final DocumentsState state; - const BulkDeleteConfirmationDialog({Key? key, required this.state}) - : super(key: key); + const BulkDeleteConfirmationDialog({ + Key? key, + required this.state, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -29,13 +29,7 @@ class BulkDeleteConfirmationDialog extends StatelessWidget { .documentsPageSelectionBulkDeleteDialogWarningTextMany, ), const SizedBox(height: 16), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 150), - child: ListView( - shrinkWrap: true, - children: state.selection.map(_buildBulletPoint).toList(), - ), - ), + ...state.selection.map(_buildBulletPoint).toList(), const SizedBox(height: 16), Text( S.of(context).documentsPageSelectionBulkDeleteDialogContinueText), @@ -61,12 +55,15 @@ class BulkDeleteConfirmationDialog extends StatelessWidget { } Widget _buildBulletPoint(DocumentModel doc) { - return Text( - "\t$_bulletPoint ${doc.title}", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w700, + return ListTile( + dense: true, + title: Text( + doc.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + ), ), ); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index e4d9af2..47257b8 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -32,6 +32,7 @@ import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:path/path.dart' as p; +import 'package:responsive_builder/responsive_builder.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -165,44 +166,114 @@ class _HomePageState extends State { }, ), ], - child: Scaffold( - key: rootScaffoldKey, - bottomNavigationBar: BottomNavBar( - selectedIndex: _currentIndex, - onNavigationChanged: (index) { - if (_currentIndex != index) { - setState(() => _currentIndex = index); - } - }, - ), - drawer: const InfoDrawer(), - body: [ - MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => DocumentsCubit( - context.read(), - context.read(), + child: ResponsiveBuilder( + builder: (context, sizingInformation) { + if (!sizingInformation.isMobile) { + return Scaffold( + key: rootScaffoldKey, + drawer: const InfoDrawer(), + body: Row(children: [ + NavigationRail( + labelType: NavigationRailLabelType.all, + destinations: [ + NavigationRailDestination( + icon: const Icon(Icons.description_outlined), + selectedIcon: Icon( + Icons.description, + color: Theme.of(context).colorScheme.primary, + ), + label: Text(S.of(context).bottomNavDocumentsPageLabel), + ), + NavigationRailDestination( + icon: const Icon(Icons.document_scanner_outlined), + selectedIcon: Icon( + Icons.document_scanner, + color: Theme.of(context).colorScheme.primary, + ), + label: Text(S.of(context).bottomNavScannerPageLabel), + ), + NavigationRailDestination( + icon: const Icon(Icons.sell_outlined), + selectedIcon: Icon( + Icons.sell, + color: Theme.of(context).colorScheme.primary, + ), + label: Text(S.of(context).bottomNavLabelsPageLabel), + ), + ], + selectedIndex: _currentIndex, + onDestinationSelected: _onNavigationChanged, ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: [ + MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => DocumentsCubit( + context.read(), + context.read(), + ), + ), + BlocProvider( + create: (context) => SavedViewCubit( + context.read(), + ), + ), + ], + child: const DocumentsPage(), + ), + BlocProvider.value( + value: _scannerCubit, + child: const ScannerPage(), + ), + const LabelsPage(), + ][_currentIndex]), + ]), + ); + } + return Scaffold( + key: rootScaffoldKey, + bottomNavigationBar: BottomNavBar( + selectedIndex: _currentIndex, + onNavigationChanged: _onNavigationChanged, + ), + drawer: const InfoDrawer(), + body: [ + MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => DocumentsCubit( + context.read(), + context.read(), + ), + ), + BlocProvider( + create: (context) => SavedViewCubit( + context.read(), + ), + ), + ], + child: const DocumentsPage(), ), - BlocProvider( - create: (context) => SavedViewCubit( - context.read(), - ), + BlocProvider.value( + value: _scannerCubit, + child: const ScannerPage(), ), - ], - child: const DocumentsPage(), - ), - BlocProvider.value( - value: _scannerCubit, - child: const ScannerPage(), - ), - const LabelsPage(), - ][_currentIndex], + const LabelsPage(), + ][_currentIndex], + ); + }, ), ); } + void _onNavigationChanged(index) { + if (_currentIndex != index) { + setState(() => _currentIndex = index); + } + } + void _initializeData(BuildContext context) { try { context.read>().findAll(); diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index eb78c68..13c3efb 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -122,6 +122,19 @@ class InboxCubit extends HydratedCubit { } } + Future assignAsn(DocumentModel document) async { + if (document.archiveSerialNumber == null) { + final int asn = await _documentsApi.findNextAsn(); + final updatedDocument = await _documentsApi + .update(document.copyWith(archiveSerialNumber: asn)); + emit( + state.copyWith( + inboxItems: state.inboxItems + .map((e) => e.id == document.id ? updatedDocument : e)), + ); + } + } + void acknowledgeHint() { emit(state.copyWith(isHintAcknowledged: true)); } diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index be56639..b4f37f6 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -3,11 +3,20 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; +import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; +import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; +import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:badges/badges.dart' as b; +import 'package:paperless_mobile/extensions/string_extensions.dart'; class InboxItem extends StatelessWidget { static const _a4AspectRatio = 1 / 1.4142; @@ -23,7 +32,31 @@ class InboxItem extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - title: Text(document.title), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Wrap( + direction: Axis.horizontal, + children: [ + Row( + children: [ + Text( + document.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ) + ], + ), + Row( + children: [], + ), + ], + ), + ), + ], + ), isThreeLine: true, leading: AspectRatio( aspectRatio: _a4AspectRatio, @@ -37,16 +70,54 @@ class InboxItem extends StatelessWidget { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(DateFormat().format(document.added)), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.person_outline, + size: Theme.of(context).textTheme.bodySmall?.fontSize, + ), + Flexible( + child: LabelText( + id: document.correspondent, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.description_outlined, + size: Theme.of(context).textTheme.bodySmall?.fontSize, + ), + Flexible( + child: LabelText( + id: document.documentType, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), TagsWidget( tagIds: document.tags, isMultiLine: false, isClickable: false, isSelectedPredicate: (_) => false, - onTagSelected: (_) {}, + showShortNames: true, + dense: true, ), ], ), + trailing: document.archiveSerialNumber != null + ? Text( + document.archiveSerialNumber!.toString(), + style: Theme.of(context).textTheme.bodySmall, + ) + : null, onTap: () async { final returnedDocument = await Navigator.push( context, diff --git a/lib/features/labels/tags/view/widgets/tag_widget.dart b/lib/features/labels/tags/view/widgets/tag_widget.dart index bd51682..4e21648 100644 --- a/lib/features/labels/tags/view/widgets/tag_widget.dart +++ b/lib/features/labels/tags/view/widgets/tag_widget.dart @@ -7,6 +7,8 @@ class TagWidget extends StatelessWidget { final VoidCallback onSelected; final bool isSelected; final bool isClickable; + final bool showShortName; + final bool dense; const TagWidget({ super.key, @@ -15,6 +17,8 @@ class TagWidget extends StatelessWidget { this.isClickable = true, required this.onSelected, required this.isSelected, + this.showShortName = false, + this.dense = false, }); @override @@ -24,13 +28,18 @@ class TagWidget extends StatelessWidget { child: AbsorbPointer( absorbing: !isClickable, child: FilterChip( + labelPadding: + dense ? const EdgeInsets.symmetric(horizontal: 2) : null, + padding: dense ? const EdgeInsets.all(4) : null, selected: isSelected, selectedColor: tag.color, onSelected: (_) => onSelected(), visualDensity: const VisualDensity(vertical: -2), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, label: Text( - tag.name, + showShortName && tag.name.length > 6 + ? '${tag.name.substring(0, 6)}...' + : tag.name, style: TextStyle(color: tag.textColor), ), checkmarkColor: tag.textColor, diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index 2fafb0b..b06c073 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -6,13 +6,15 @@ import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; -class TagsWidget extends StatefulWidget { +class TagsWidget extends StatelessWidget { final Iterable tagIds; final bool isMultiLine; final VoidCallback? afterTagTapped; final void Function(int tagId)? onTagSelected; final bool isClickable; final bool Function(int id) isSelectedPredicate; + final bool showShortNames; + final bool dense; const TagsWidget({ Key? key, @@ -21,32 +23,31 @@ class TagsWidget extends StatefulWidget { this.isMultiLine = true, this.isClickable = true, required this.isSelectedPredicate, - required this.onTagSelected, + this.onTagSelected, + this.showShortNames = false, + this.dense = false, }) : super(key: key); - @override - State createState() => _TagsWidgetState(); -} - -class _TagsWidgetState extends State { @override Widget build(BuildContext context) { return TagBlocProvider( child: BlocBuilder, LabelState>( builder: (context, state) { - final children = widget.tagIds + final children = tagIds .where((id) => state.labels.containsKey(id)) .map( (id) => TagWidget( tag: state.getLabel(id)!, - afterTagTapped: widget.afterTagTapped, - isClickable: widget.isClickable, - isSelected: widget.isSelectedPredicate(id), - onSelected: () => widget.onTagSelected?.call(id), + afterTagTapped: afterTagTapped, + isClickable: isClickable, + isSelected: isSelectedPredicate(id), + onSelected: () => onTagSelected?.call(id), + showShortName: showShortNames, + dense: dense, ), ) .toList(); - if (widget.isMultiLine) { + if (isMultiLine) { return Wrap( runAlignment: WrapAlignment.start, children: children, diff --git a/lib/features/labels/view/widgets/label_text.dart b/lib/features/labels/view/widgets/label_text.dart new file mode 100644 index 0000000..53b25ff --- /dev/null +++ b/lib/features/labels/view/widgets/label_text.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; + +class LabelText + extends StatelessWidget { + final int? id; + final String placeholder; + final TextStyle? style; + + const LabelText({ + super.key, + this.style, + this.id, + this.placeholder = "", + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + context.read>(), + ), + child: BlocBuilder, LabelState>( + builder: (context, state) { + return Text( + state.labels[id]?.toString() ?? placeholder, + style: style, + ); + }, + ), + ); + ; + } +} diff --git a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart index adee81e..1e4845e 100644 --- a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -6,7 +8,7 @@ import 'package:paperless_mobile/generated/l10n.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; - final void Function(String address) onDone; + final void Function(String? address) onDone; const ServerAddressFormField({ Key? key, required this.onDone, @@ -31,7 +33,7 @@ class _ServerAddressFormFieldState extends State { }); } - final TextEditingController _textEditingController = TextEditingController(); + final _textEditingController = TextEditingController(); @override Widget build(BuildContext context) { @@ -56,7 +58,7 @@ class _ServerAddressFormFieldState extends State { labelText: S.of(context).loginPageServerUrlFieldLabel, suffixIcon: _canClear ? IconButton( - icon: Icon(Icons.clear), + icon: const Icon(Icons.clear), color: Theme.of(context).iconTheme.color, onPressed: () { _textEditingController.clear(); @@ -64,18 +66,14 @@ class _ServerAddressFormFieldState extends State { ) : null, ), - onSubmitted: (value) { - if (value == null) return; - // Remove trailing slash if it is a valid address. - String address = value.trim(); - address = _replaceTrailingSlashes(address); - _textEditingController.text = address; - widget.onDone(address); - }, + onSubmitted: (_) => _formatInput(), ); } - String _replaceTrailingSlashes(String src) { - return src.replaceAll(RegExp(r'^\/+|\/+$'), ''); + void _formatInput() { + String address = _textEditingController.text.trim(); + address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); + _textEditingController.text = address; + widget.onDone(address); } } diff --git a/lib/features/login/view/widgets/login_pages/server_connection_page.dart b/lib/features/login/view/widgets/login_pages/server_connection_page.dart index ca00c0a..e1cfd54 100644 --- a/lib/features/login/view/widgets/login_pages/server_connection_page.dart +++ b/lib/features/login/view/widgets/login_pages/server_connection_page.dart @@ -58,8 +58,12 @@ class _ServerConnectionPageState extends State { ), bottomNavigationBar: BottomAppBar( child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + TextButton( + child: Text("Test connection"), + onPressed: _updateReachability, + ), FilledButton( child: Text(S.of(context).loginPageContinueLabel), onPressed: _reachabilityStatus == ReachabilityStatus.reachable @@ -76,6 +80,7 @@ class _ServerConnectionPageState extends State { setState(() { _isCheckingConnection = true; }); + final status = await context .read() .isPaperlessServerReachable( diff --git a/lib/features/notifications/services/local_notification_service.dart b/lib/features/notifications/services/local_notification_service.dart index c1ef3d5..a30893c 100644 --- a/lib/features/notifications/services/local_notification_service.dart +++ b/lib/features/notifications/services/local_notification_service.dart @@ -32,6 +32,10 @@ class LocalNotificationService { initializationSettings, onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, ); + await _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestPermission(); } //TODO: INTL diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index ecb09e1..12c5e3c 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -142,31 +142,29 @@ class _ScannerPageState extends State final file = await _assembleFileBytes( context.read().state, ); - final taskId = await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => LabelRepositoriesProvider( - child: BlocProvider( - create: (context) => DocumentUploadCubit( - localVault: context.read(), - documentApi: context.read(), - correspondentRepository: context.read< - LabelRepository>(), - documentTypeRepository: context.read< - LabelRepository>(), - tagRepository: - context.read>(), - ), - child: DocumentUploadPreparationPage( - fileBytes: file.bytes, - fileExtension: file.extension, - ), - ), + final taskId = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => LabelRepositoriesProvider( + child: BlocProvider( + create: (context) => DocumentUploadCubit( + localVault: context.read(), + documentApi: context.read(), + correspondentRepository: context.read< + LabelRepository>(), + documentTypeRepository: context.read< + LabelRepository>(), + tagRepository: + context.read>(), + ), + child: DocumentUploadPreparationPage( + fileBytes: file.bytes, + fileExtension: file.extension, ), ), - ) ?? - false; + ), + ), + ); if (taskId != null) { // For paperless version older than 1.11.3, task id will always be null! context.read().reset(); diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 59925e8..a736d15 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -563,5 +563,6 @@ "verifyIdentityPageTitle": "", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "inboxPageAssignAsnLabel": "Assign ASN" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 46d24a7..6819885 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -563,5 +563,6 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "inboxPageAssignAsnLabel": "Assign ASN" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4f41aeb..eb76a5a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -563,5 +563,6 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "inboxPageAssignAsnLabel": "Assign ASN" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 548b554..5c302a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -113,8 +113,9 @@ void main() async { authApi, sessionManager, ); - await authCubit - .restoreSessionState(appSettingsCubit.state.isLocalAuthenticationEnabled); + await authCubit.restoreSessionState( + appSettingsCubit.state.isLocalAuthenticationEnabled, + ); if (authCubit.state.isAuthenticated) { final auth = authCubit.state.authentication!; diff --git a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart index 777e8d6..78b1123 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'query_type.dart'; @@ -5,7 +6,7 @@ import 'query_type.dart'; part 'text_query.g.dart'; @JsonSerializable() -class TextQuery { +class TextQuery extends Equatable { final QueryType queryType; final String? queryText; @@ -61,4 +62,7 @@ class TextQuery { factory TextQuery.fromJson(Map json) => _$TextQueryFromJson(json); + + @override + List get props => [queryType, queryText]; } diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 8ff4764..d294cd4 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -219,12 +219,12 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { final response = await client.get( '/api/search/autocomplete/', queryParameters: { - 'query': query, + 'term': query, 'limit': limit, }, ); if (response.statusCode == 200) { - return response.data as List; + return (response.data as List).cast(); } throw const PaperlessServerException(ErrorCode.autocompleteQueryError); } on DioError catch (err) { diff --git a/pubspec.lock b/pubspec.lock index 6bed59a..8f1f30e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1292,6 +1292,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.5" + responsive_builder: + dependency: "direct main" + description: + name: responsive_builder + sha256: f01bc341c73b6db7bd6319e22d2c160f28f924399ae46e6699ecc8160ba2765c + url: "https://pub.dev" + source: hosted + version: "0.4.3" rxdart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e035d1b..f51ab43 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,7 @@ dependencies: device_info_plus: ^4.1.3 flutter_local_notifications: ^13.0.0 flutter_staggered_grid_view: ^0.6.2 + responsive_builder: ^0.4.3 dev_dependencies: integration_test: