diff --git a/android/fastlane/metadata/android/de-DE/changelogs/54.txt b/android/fastlane/metadata/android/de-DE/changelogs/54.txt new file mode 100644 index 0000000..5fcca68 --- /dev/null +++ b/android/fastlane/metadata/android/de-DE/changelogs/54.txt @@ -0,0 +1,4 @@ +* Neu: App-Logs werden in Dateien geschrieben und können auch direkt in der App eingesehen werden +* Optimierung der Datums-Eingabe durch neues Eingabemaske +* Schneller Wechsel zwischen Dokumenten-PDF Ansicht und Bearbeitungsmaske +* Kleinere Visuelle Anpassungen und Bugfixes \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/54.txt b/android/fastlane/metadata/android/en-US/changelogs/54.txt new file mode 100644 index 0000000..3e8e25e --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/54.txt @@ -0,0 +1,4 @@ +* New: App-Logs are written to local files and can also be viewed in-app +* New and optimized date input fields +* Quickly switch between editing your document and a PDF-View +* Minor visual changes and bug fixes \ No newline at end of file diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/lib/core/interceptor/dio_http_error_interceptor.dart index 6c8fb1b..fd5ea63 100644 --- a/lib/core/interceptor/dio_http_error_interceptor.dart +++ b/lib/core/interceptor/dio_http_error_interceptor.dart @@ -38,6 +38,8 @@ class DioHttpErrorInterceptor extends Interceptor { const PaperlessApiException(ErrorCode.missingClientCertificate), ), ); + } else { + handler.reject(err); } } } diff --git a/lib/core/notifier/document_changed_notifier.dart b/lib/core/notifier/document_changed_notifier.dart index f992ca3..22aa023 100644 --- a/lib/core/notifier/document_changed_notifier.dart +++ b/lib/core/notifier/document_changed_notifier.dart @@ -27,14 +27,15 @@ class DocumentChangedNotifier { Object subscriber, { DocumentChangedCallback? onUpdated, DocumentChangedCallback? onDeleted, + Iterable? ids, }) { _subscribers.putIfAbsent( subscriber, () => [ - _updated.listen((value) { + _updated.where((doc) => ids?.contains(doc.id) ?? true).listen((value) { onUpdated?.call(value); }), - _deleted.listen((value) { + _deleted.where((doc) => ids?.contains(doc.id) ?? true).listen((value) { onDeleted?.call(value); }), ], diff --git a/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart b/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart index 2d25a92..eb1ab5f 100644 --- a/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart +++ b/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart @@ -53,6 +53,7 @@ class FormBuilderLocalizedDatePicker extends StatefulWidget { final DateTime? initialValue; final DateTime firstDate; final DateTime lastDate; + final FocusNode? focusNode; /// If set to true, the field will not throw any validation errors when empty. final bool allowUnset; @@ -67,6 +68,7 @@ class FormBuilderLocalizedDatePicker extends StatefulWidget { required this.labelText, this.prefixIcon, this.allowUnset = false, + this.focusNode, }); @override @@ -100,8 +102,11 @@ class _FormBuilderLocalizedDatePickerState final initialText = widget.initialValue != null ? DateFormat(formatString).format(widget.initialValue!) : null; + final defaultFocusNode = FocusNode(debugLabel: formatString); + final focusNode = + i == 0 ? (widget.focusNode ?? defaultFocusNode) : defaultFocusNode; final controls = _NeighbourAwareDateInputSegmentControls( - node: FocusNode(debugLabel: formatString), + node: focusNode, controller: TextEditingController(text: initialText), format: formatString, position: i, diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart index ca4fdf2..28e3877 100644 --- a/lib/features/app_drawer/view/app_drawer.dart +++ b/lib/features/app_drawer/view/app_drawer.dart @@ -106,14 +106,6 @@ class AppDrawer extends StatelessWidget { ); }, ), - ListTile( - dense: true, - leading: const Icon(Icons.history), - title: Text(S.of(context)!.changelog), - onTap: () { - ChangelogRoute().push(context); - }, - ), ListTile( dense: true, leading: const Icon(Icons.bug_report_outlined), @@ -179,14 +171,6 @@ class AppDrawer extends StatelessWidget { .fade(duration: 1.seconds, begin: 1, end: 0.3); }, ), - ListTile( - dense: true, - leading: const Icon(Icons.subject), - title: Text(S.of(context)!.appLogs('')), - onTap: () { - AppLogsRoute().push(context); - }, - ), ListTile( dense: true, leading: const Icon(Icons.settings_outlined), diff --git a/lib/features/changelogs/view/changelog_dialog.dart b/lib/features/changelogs/view/changelog_dialog.dart index 7ebfa9e..bad8155 100644 --- a/lib/features/changelogs/view/changelog_dialog.dart +++ b/lib/features/changelogs/view/changelog_dialog.dart @@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget { } const _versionNumbers = { + "54": "3.0.7", "53": "3.0.6", "52": "3.0.5", "51": "3.0.4", diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index 4b38265..466d911 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -31,14 +31,13 @@ class DocumentDetailsCubit extends Cubit { this._notificationService, { required this.id, }) : super(const DocumentDetailsInitial()) { - _notifier.addListener(this, onUpdated: (document) { - if (state is DocumentDetailsLoaded) { - final currentState = state as DocumentDetailsLoaded; - if (document.id == currentState.document.id) { - replace(document); - } - } - }); + _notifier.addListener( + this, + onUpdated: (document) { + replace(document); + }, + ids: [id], + ); } Future initialize() async { diff --git a/lib/features/document_edit/cubit/document_edit_cubit.dart b/lib/features/document_edit/cubit/document_edit_cubit.dart index 850f41a..839bf98 100644 --- a/lib/features/document_edit/cubit/document_edit_cubit.dart +++ b/lib/features/document_edit/cubit/document_edit_cubit.dart @@ -22,11 +22,13 @@ class DocumentEditCubit extends Cubit { required DocumentModel document, }) : _initialDocument = document, super(DocumentEditState(document: document)) { - _notifier.addListener(this, onUpdated: (doc) { - if (doc.id == document.id) { + _notifier.addListener( + this, + onUpdated: (doc) { emit(state.copyWith(document: doc)); - } - }); + }, + ids: [document.id], + ); } Future updateDocument(DocumentModel document) async { diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index 2acf274..18a7e32 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -9,13 +9,13 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; -import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; +import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -32,7 +32,8 @@ class DocumentEditPage extends StatefulWidget { State createState() => _DocumentEditPageState(); } -class _DocumentEditPageState extends State { +class _DocumentEditPageState extends State + with SingleTickerProviderStateMixin { static const fkTitle = "title"; static const fkCorrespondent = "correspondent"; static const fkTags = "tags"; @@ -43,6 +44,23 @@ class _DocumentEditPageState extends State { final _formKey = GlobalKey(); + bool _isShowingPdf = false; + + late final AnimationController _animationController; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _animation = + CurvedAnimation(parent: _animationController, curve: Curves.easeInCubic) + .drive(Tween(begin: 0, end: 1)); + } + @override Widget build(BuildContext context) { final currentUser = context.watch().paperlessUser; @@ -75,197 +93,228 @@ class _DocumentEditPageState extends State { doc.created != createdAt || (doc.content != content && isContentTouched); }, - child: DefaultTabController( - length: 2, + child: FormBuilder( + key: _formKey, child: Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton.extended( - heroTag: "fab_document_edit", - onPressed: () => _onSubmit(state.document), - icon: const Icon(Icons.save), - label: Text(S.of(context)!.saveChanges), - ), - appBar: AppBar( - title: Text(S.of(context)!.editDocument), - bottom: TabBar( - tabs: [ - Tab(text: S.of(context)!.overview), - Tab(text: S.of(context)!.content) - ], - ), - ), - extendBody: true, - body: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - top: 8, - left: 8, - right: 8, - ), - child: FormBuilder( - key: _formKey, - child: TabBarView( - children: [ - ListView( - children: [ - _buildTitleFormField(state.document.title).padded(), - _buildCreatedAtFormField( - state.document.created, - filteredSuggestions, - ).padded(), - // Correspondent form field - if (currentUser.canViewCorrespondents) - Column( - children: [ - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - onAddLabel: (currentInput) => - CreateLabelRoute( - LabelType.correspondent, - name: currentInput, - ).push(context), - addLabelText: - S.of(context)!.addCorrespondent, - labelText: S.of(context)!.correspondent, - options: context - .watch() - .state - .correspondents, - initialValue: state - .document.correspondent != - null - ? SetIdQueryParameter( - id: state.document.correspondent!) - : const UnsetIdQueryParameter(), - name: fkCorrespondent, - prefixIcon: - const Icon(Icons.person_outlined), - allowSelectUnassigned: true, - canCreateNewLabel: - currentUser.canCreateCorrespondents, - suggestions: - filteredSuggestions?.correspondents ?? - [], - ), - ], - ).padded(), - // DocumentType form field - if (currentUser.canViewDocumentTypes) - Column( - children: [ - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - onAddLabel: (currentInput) => - CreateLabelRoute( - LabelType.documentType, - name: currentInput, - ).push(context), - canCreateNewLabel: - currentUser.canCreateDocumentTypes, - addLabelText: - S.of(context)!.addDocumentType, - labelText: S.of(context)!.documentType, - initialValue: state.document.documentType != - null - ? SetIdQueryParameter( - id: state.document.documentType!) - : const UnsetIdQueryParameter(), - options: context - .watch() - .state - .documentTypes, - name: _DocumentEditPageState.fkDocumentType, - prefixIcon: - const Icon(Icons.description_outlined), - allowSelectUnassigned: true, - suggestions: - filteredSuggestions?.documentTypes ?? - [], - ), - ], - ).padded(), - // StoragePath form field - if (currentUser.canViewStoragePaths) - Column( - children: [ - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - onAddLabel: (currentInput) => - CreateLabelRoute( - LabelType.storagePath, - name: currentInput, - ).push(context), - canCreateNewLabel: - currentUser.canCreateStoragePaths, - addLabelText: S.of(context)!.addStoragePath, - labelText: S.of(context)!.storagePath, - options: context - .watch() - .state - .storagePaths, - initialValue: - state.document.storagePath != null - ? SetIdQueryParameter( - id: state.document.storagePath!) - : const UnsetIdQueryParameter(), - name: fkStoragePath, - prefixIcon: - const Icon(Icons.folder_outlined), - allowSelectUnassigned: true, - ), - ], - ).padded(), - // Tag form field - if (currentUser.canViewTags) - TagsFormField( - options: - context.watch().state.tags, - name: fkTags, - allowOnlySelection: true, - allowCreation: true, - allowExclude: false, - suggestions: filteredSuggestions?.tags ?? [], - initialValue: IdsTagsQuery( - include: state.document.tags.toList(), - ), - ).padded(), - if (filteredSuggestions?.tags - .toSet() - .difference(state.document.tags.toSet()) - .isNotEmpty ?? - false) - const SizedBox(height: 64), - ], - ), - SingleChildScrollView( - child: Column( - children: [ - FormBuilderTextField( - name: fkContent, - maxLines: null, - keyboardType: TextInputType.multiline, - initialValue: state.document.content, - decoration: const InputDecoration( - border: InputBorder.none, - ), - ), - const SizedBox(height: 84), - ], - ), - ), - ], + appBar: AppBar( + title: Text(S.of(context)!.editDocument), + actions: [ + IconButton( + tooltip: _isShowingPdf + ? S.of(context)!.hidePdf + : S.of(context)!.showPdf, + padding: EdgeInsets.all(12), + icon: AnimatedCrossFade( + duration: _animationController.duration!, + reverseDuration: _animationController.reverseDuration, + crossFadeState: _isShowingPdf + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Icon(Icons.visibility_off_outlined), + secondChild: Icon(Icons.visibility_outlined), + ), + onPressed: () { + if (_isShowingPdf) { + setState(() { + _isShowingPdf = false; + }); + _animationController.reverse(); + } else { + setState(() { + _isShowingPdf = true; + }); + _animationController.forward(); + } + }, + ) + ], + ), + body: Stack( + children: [ + DefaultTabController( + length: 2, + child: Scaffold( + resizeToAvoidBottomInset: true, + floatingActionButton: !_isShowingPdf + ? FloatingActionButton.extended( + heroTag: "fab_document_edit", + onPressed: () => _onSubmit(state.document), + icon: const Icon(Icons.save), + label: Text(S.of(context)!.saveChanges), + ) + : null, + appBar: TabBar( + tabs: [ + Tab(text: S.of(context)!.overview), + Tab(text: S.of(context)!.content), + ], + ), + extendBody: true, + body: _buildEditForm( + context, + state, + filteredSuggestions, + currentUser, + ), ), ), - )), + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.scale( + alignment: Alignment.bottomLeft, + scale: _animation.value, + child: DocumentView( + showAppBar: false, + showControls: false, + documentBytes: context + .read() + .downloadDocument(state.document.id), + ), + ); + }, + ), + ], + ), + ), ), ); }, ); } + Padding _buildEditForm(BuildContext context, DocumentEditState state, + FieldSuggestions? filteredSuggestions, UserModel currentUser) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TabBarView( + physics: NeverScrollableScrollPhysics(), + children: [ + ListView( + children: [ + SizedBox(height: 16), + _buildTitleFormField(state.document.title).padded(), + _buildCreatedAtFormField( + state.document.created, + filteredSuggestions, + ).padded(), + // Correspondent form field + if (currentUser.canViewCorrespondents) + Column( + children: [ + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + onAddLabel: (currentInput) => CreateLabelRoute( + LabelType.correspondent, + name: currentInput, + ).push(context), + addLabelText: S.of(context)!.addCorrespondent, + labelText: S.of(context)!.correspondent, + options: + context.watch().state.correspondents, + initialValue: state.document.correspondent != null + ? SetIdQueryParameter( + id: state.document.correspondent!) + : const UnsetIdQueryParameter(), + name: fkCorrespondent, + prefixIcon: const Icon(Icons.person_outlined), + allowSelectUnassigned: true, + canCreateNewLabel: currentUser.canCreateCorrespondents, + suggestions: filteredSuggestions?.correspondents ?? [], + ), + ], + ).padded(), + // DocumentType form field + if (currentUser.canViewDocumentTypes) + Column( + children: [ + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + onAddLabel: (currentInput) => CreateLabelRoute( + LabelType.documentType, + name: currentInput, + ).push(context), + canCreateNewLabel: currentUser.canCreateDocumentTypes, + addLabelText: S.of(context)!.addDocumentType, + labelText: S.of(context)!.documentType, + initialValue: state.document.documentType != null + ? SetIdQueryParameter( + id: state.document.documentType!) + : const UnsetIdQueryParameter(), + options: + context.watch().state.documentTypes, + name: _DocumentEditPageState.fkDocumentType, + prefixIcon: const Icon(Icons.description_outlined), + allowSelectUnassigned: true, + suggestions: filteredSuggestions?.documentTypes ?? [], + ), + ], + ).padded(), + // StoragePath form field + if (currentUser.canViewStoragePaths) + Column( + children: [ + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + onAddLabel: (currentInput) => CreateLabelRoute( + LabelType.storagePath, + name: currentInput, + ).push(context), + canCreateNewLabel: currentUser.canCreateStoragePaths, + addLabelText: S.of(context)!.addStoragePath, + labelText: S.of(context)!.storagePath, + options: + context.watch().state.storagePaths, + initialValue: state.document.storagePath != null + ? SetIdQueryParameter(id: state.document.storagePath!) + : const UnsetIdQueryParameter(), + name: fkStoragePath, + prefixIcon: const Icon(Icons.folder_outlined), + allowSelectUnassigned: true, + ), + ], + ).padded(), + // Tag form field + if (currentUser.canViewTags) + TagsFormField( + options: context.watch().state.tags, + name: fkTags, + allowOnlySelection: true, + allowCreation: true, + allowExclude: false, + suggestions: filteredSuggestions?.tags ?? [], + initialValue: IdsTagsQuery( + include: state.document.tags.toList(), + ), + ).padded(), + + const SizedBox(height: 140), + ], + ), + SingleChildScrollView( + child: Column( + children: [ + FormBuilderTextField( + name: fkContent, + maxLines: null, + keyboardType: TextInputType.multiline, + initialValue: state.document.content, + decoration: const InputDecoration( + border: InputBorder.none, + ), + ), + const SizedBox(height: 84), + ], + ), + ), + ], + ), + ); + } + ( String? title, int? correspondent, diff --git a/lib/features/documents/view/pages/document_view.dart b/lib/features/documents/view/pages/document_view.dart index 19d4fb9..bda53a1 100644 --- a/lib/features/documents/view/pages/document_view.dart +++ b/lib/features/documents/view/pages/document_view.dart @@ -5,9 +5,13 @@ import 'package:flutter_pdfview/flutter_pdfview.dart'; class DocumentView extends StatefulWidget { final Future documentBytes; final String? title; + final bool showAppBar; + final bool showControls; const DocumentView({ Key? key, required this.documentBytes, + this.showAppBar = true, + this.showControls = true, this.title, }) : super(key: key); @@ -27,43 +31,47 @@ class _DocumentViewState extends State { final canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!; final canGoToPreviousPage = isInitialized && _currentPage! > 0; return Scaffold( - appBar: AppBar( - title: widget.title != null ? Text(widget.title!) : null, - ), - bottomNavigationBar: BottomAppBar( - child: Row( - children: [ - Flexible( + appBar: widget.showAppBar + ? AppBar( + title: widget.title != null ? Text(widget.title!) : null, + ) + : null, + bottomNavigationBar: widget.showControls + ? BottomAppBar( child: Row( children: [ - IconButton.filled( - onPressed: canGoToPreviousPage - ? () { - _controller?.setPage(_currentPage! - 1); - } - : null, - icon: const Icon(Icons.arrow_left), - ), - const SizedBox(width: 16), - IconButton.filled( - onPressed: canGoToNextPage - ? () { - _controller?.setPage(_currentPage! + 1); - } - : null, - icon: const Icon(Icons.arrow_right), + Flexible( + child: Row( + children: [ + IconButton.filled( + onPressed: canGoToPreviousPage + ? () { + _controller?.setPage(_currentPage! - 1); + } + : null, + icon: const Icon(Icons.arrow_left), + ), + const SizedBox(width: 16), + IconButton.filled( + onPressed: canGoToNextPage + ? () { + _controller?.setPage(_currentPage! + 1); + } + : null, + icon: const Icon(Icons.arrow_right), + ), + ], + ), ), + if (_currentPage != null && _totalPages != null) + Text( + "${_currentPage! + 1}/$_totalPages", + style: Theme.of(context).textTheme.labelLarge, + ), ], ), - ), - if (_currentPage != null && _totalPages != null) - Text( - "${_currentPage! + 1}/$_totalPages", - style: Theme.of(context).textTheme.labelLarge, - ), - ], - ), - ), + ) + : null, body: FutureBuilder( future: widget.documentBytes, builder: (context, snapshot) { @@ -93,12 +101,7 @@ class _DocumentViewState extends State { onViewCreated: (controller) { _controller = controller; }, - onError: (error) { - print(error.toString()); - }, - onPageError: (page, error) { - print('$page: ${error.toString()}'); - }, + ); }), ); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 7e7fd99..addcfad 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -110,7 +110,7 @@ class _DocumentsPageState extends State { } void _scrollExtentChangedListener() { - const threshold = 400; + const threshold = kToolbarHeight * 2; final offset = _nestedScrollViewKey.currentState!.innerController.position.pixels; if (offset < threshold && _showExtendedFab == false) { @@ -429,6 +429,9 @@ class _DocumentsPageState extends State { ); }, ), + const SliverToBoxAdapter( + child: SizedBox(height: 96), + ) ], ), ), diff --git a/lib/features/documents/view/widgets/adaptive_documents_view.dart b/lib/features/documents/view/widgets/adaptive_documents_view.dart index 4c861e8..f9e4de3 100644 --- a/lib/features/documents/view/widgets/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart'; @@ -159,7 +160,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { crossAxisCount: 2, mainAxisSpacing: 4, crossAxisSpacing: 4, - mainAxisExtent: 356, + mainAxisExtent: 324, ), itemCount: documents.length, itemBuilder: (context, index) { @@ -176,7 +177,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, - ); + ).paddedSymmetrically(horizontal: 4); }, ); } diff --git a/lib/features/documents/view/widgets/date_and_document_type_widget.dart b/lib/features/documents/view/widgets/date_and_document_type_widget.dart new file mode 100644 index 0000000..f9da206 --- /dev/null +++ b/lib/features/documents/view/widgets/date_and_document_type_widget.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:provider/provider.dart'; + +class DateAndDocumentTypeLabelWidget extends StatelessWidget { + const DateAndDocumentTypeLabelWidget({ + super.key, + required this.document, + required this.onDocumentTypeSelected, + }); + + final DocumentModel document; + final void Function(int? documentTypeId)? onDocumentTypeSelected; + + @override + Widget build(BuildContext context) { + final subtitleStyle = + Theme.of(context).textTheme.labelMedium?.apply(color: Colors.grey); + return RichText( + maxLines: 1, + overflow: TextOverflow.ellipsis, + text: TextSpan( + text: DateFormat.yMMMMd(Localizations.localeOf(context).toString()) + .format(document.created), + style: subtitleStyle, + children: document.documentType != null + ? [ + const TextSpan(text: '\u30FB'), + WidgetSpan( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(4), + onTap: onDocumentTypeSelected != null + ? () => onDocumentTypeSelected!(document.documentType) + : null, + child: Text( + context + .watch() + .state + .documentTypes[document.documentType]! + .name, + style: subtitleStyle, + ), + ), + ), + ), + ] + : null, + ), + ); + } +} diff --git a/lib/features/documents/view/widgets/items/document_detailed_item.dart b/lib/features/documents/view/widgets/items/document_detailed_item.dart index 127457d..0410e61 100644 --- a/lib/features/documents/view/widgets/items/document_detailed_item.dart +++ b/lib/features/documents/view/widgets/items/document_detailed_item.dart @@ -11,6 +11,7 @@ import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; @@ -100,38 +101,28 @@ class DocumentDetailedItem extends DocumentItem { ], ), ), + if (paperlessUser.canViewCorrespondents) + CorrespondentWidget( + onSelected: onCorrespondentSelected, + textStyle: Theme.of(context).textTheme.titleSmall?.apply( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + correspondent: labels.correspondents[document.correspondent], + ).paddedLTRB(8, 8, 8, 0), + Text( + document.title.isEmpty ? '(-)' : document.title, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ).paddedLTRB(8, 8, 8, 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: RichText( - maxLines: 1, - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: Theme.of(context) - .textTheme - .bodySmall - ?.apply(color: Theme.of(context).hintColor), - text: DateFormat.yMMMMd( - Localizations.localeOf(context).toString()) - .format(document.created), - children: [ - if (paperlessUser.canViewDocumentTypes && - document.documentType != null) ...[ - const TextSpan(text: '\u30FB'), - TextSpan( - text: labels - .documentTypes[document.documentType]?.name, - recognizer: onDocumentTypeSelected != null - ? (TapGestureRecognizer() - ..onTap = () => onDocumentTypeSelected!( - document.documentType)) - : null, - ), - ], - ], - ), + child: DateAndDocumentTypeLabelWidget( + document: document, + onDocumentTypeSelected: onDocumentTypeSelected, ), ), if (document.archiveSerialNumber != null) @@ -143,30 +134,7 @@ class DocumentDetailedItem extends DocumentItem { ?.apply(color: Theme.of(context).hintColor), ), ], - ).paddedLTRB(8, 8, 8, 4), - Text( - document.title.isEmpty ? '(-)' : document.title, - style: Theme.of(context).textTheme.titleMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ).paddedLTRB(8, 0, 8, 4), - if (paperlessUser.canViewCorrespondents) - Row( - children: [ - const Icon( - Icons.person_outline, - size: 16, - ).paddedOnly(right: 4.0), - CorrespondentWidget( - onSelected: onCorrespondentSelected, - textStyle: Theme.of(context).textTheme.titleSmall?.apply( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - correspondent: - labels.correspondents[document.correspondent], - ), - ], - ).paddedLTRB(8, 0, 8, 8), + ).paddedLTRB(8, 4, 8, 8), if (highlights != null) Html( data: '

${highlights!}

', diff --git a/lib/features/documents/view/widgets/items/document_grid_item.dart b/lib/features/documents/view/widgets/items/document_grid_item.dart index b727f3a..5def307 100644 --- a/lib/features/documents/view/widgets/items/document_grid_item.dart +++ b/lib/features/documents/view/widgets/items/document_grid_item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; @@ -29,111 +30,133 @@ class DocumentGridItem extends DocumentItem { @override Widget build(BuildContext context) { var currentUser = context.watch().paperlessUser; - return Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - elevation: 1.0, - color: isSelected - ? Theme.of(context).colorScheme.inversePrimary - : Theme.of(context).cardColor, - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: _onTap, - onLongPress: onSelected != null ? () => onSelected!(document) : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: 1, - child: Stack( - children: [ - Positioned.fill( - child: DocumentPreview( - documentId: document.id, - borderRadius: 12.0, - enableHero: enableHeroAnimation, - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: SizedBox( - height: 48, - child: NotificationListener( - // Prevents ancestor notification listeners to be notified when this widget scrolls - onNotification: (notification) => true, - child: CustomScrollView( - scrollDirection: Axis.horizontal, - slivers: [ - const SliverToBoxAdapter( - child: SizedBox(width: 8), - ), - if (currentUser.canViewTags) - TagsWidget.sliver( - tags: document.tags - .map((e) => context - .watch() - .state - .tags[e]!) - .toList(), - onTagSelected: onTagSelected, - ), - const SliverToBoxAdapter( - child: SizedBox(width: 8), - ), - ], + return Stack( + children: [ + Card( + elevation: 1.0, + color: isSelected + ? Theme.of(context).colorScheme.inversePrimary + : Theme.of(context).cardColor, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _onTap, + onLongPress: + onSelected != null ? () => onSelected!(document) : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: AspectRatio( + aspectRatio: 1, + child: Stack( + children: [ + Positioned.fill( + child: DocumentPreview( + documentId: document.id, + borderRadius: 12.0, + enableHero: enableHeroAnimation, ), ), - ), + Align( + alignment: Alignment.bottomLeft, + child: SizedBox( + height: kMinInteractiveDimension, + child: NotificationListener( + // Prevents ancestor notification listeners to be notified when this widget scrolls + onNotification: (notification) => true, + child: CustomScrollView( + scrollDirection: Axis.horizontal, + slivers: [ + const SliverToBoxAdapter( + child: SizedBox(width: 8), + ), + if (currentUser.canViewTags) + TagsWidget.sliver( + tags: document.tags + .map((e) => context + .watch() + .state + .tags[e]!) + .toList(), + onTagSelected: onTagSelected, + ), + const SliverToBoxAdapter( + child: SizedBox(width: 8), + ), + ], + ), + ), + ), + ), + ], ), - ], - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (currentUser.canViewCorrespondents) - CorrespondentWidget( - correspondent: context - .watch() - .state - .correspondents[document.correspondent], - onSelected: onCorrespondentSelected, - ), - if (currentUser.canViewDocumentTypes) - DocumentTypeWidget( - documentType: context - .watch() - .state - .documentTypes[document.documentType], - onSelected: onDocumentTypeSelected, - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - document.title.isEmpty ? '-' : document.title, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - const Spacer(), - Text( - DateFormat.yMMMMd( - Localizations.localeOf(context).toString()) - .format(document.created), - style: Theme.of(context).textTheme.bodySmall, - ), - ], ), ), - ), - ], + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (currentUser.canViewCorrespondents) + CorrespondentWidget( + correspondent: context + .watch() + .state + .correspondents[document.correspondent], + onSelected: onCorrespondentSelected, + ), + if (currentUser.canViewDocumentTypes) + DocumentTypeWidget( + documentType: context + .watch() + .state + .documentTypes[document.documentType], + onSelected: onDocumentTypeSelected, + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + document.title.isEmpty ? '-' : document.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + DateFormat.yMMMMd( + Localizations.localeOf(context).toString(), + ).format(document.created), + style: Theme.of(context).textTheme.bodySmall, + ), + if (document.archiveSerialNumber != null) + Text( + '#' + document.archiveSerialNumber!.toString(), + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + ), + ) + ], + ), + ], + ), + ), + ), + ], + ), ), ), - ), + ], ); } diff --git a/lib/features/documents/view/widgets/items/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart index 2767288..25cc629 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -1,7 +1,9 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:paperless_api/src/models/document_model.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/label_repository_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; @@ -31,6 +33,7 @@ class DocumentListItem extends DocumentItem { @override Widget build(BuildContext context) { final labels = context.watch().state; + return ListTile( tileColor: backgroundColor, dense: true, @@ -75,35 +78,11 @@ class DocumentListItem extends DocumentItem { ), ], ), - subtitle: IntrinsicWidth( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RichText( - maxLines: 1, - overflow: TextOverflow.ellipsis, - text: TextSpan( - text: - DateFormat.yMMMMd(Localizations.localeOf(context).toString()) - .format(document.created), - style: Theme.of(context) - .textTheme - .labelSmall - ?.apply(color: Colors.grey), - children: document.documentType != null - ? [ - const TextSpan(text: '\u30FB'), - TextSpan( - text: labels.documentTypes[document.documentType]?.name, - recognizer: onDocumentTypeSelected != null - ? (TapGestureRecognizer() - ..onTap = () => onDocumentTypeSelected!( - document.documentType)) - : null, - ), - ] - : null, - ), - ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: DateAndDocumentTypeLabelWidget( + document: document, + onDocumentTypeSelected: onDocumentTypeSelected, ), ), isThreeLine: document.tags.isNotEmpty, diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 8d357c4..4aa4d59 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -21,15 +21,19 @@ class CorrespondentWidget extends StatelessWidget { Widget build(BuildContext context) { return AbsorbPointer( absorbing: !isClickable, - child: GestureDetector( - onTap: () => onSelected?.call(correspondent?.id), - child: Text( - correspondent?.name ?? "-", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: - (textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith( - color: textColor ?? Theme.of(context).colorScheme.primary, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(4), + onTap: () => onSelected?.call(correspondent?.id), + child: Text( + correspondent?.name ?? "-", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + (textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith( + color: textColor ?? Theme.of(context).colorScheme.primary, + ), ), ), ), diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index 3800766..1567df0 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -18,14 +18,18 @@ class DocumentTypeWidget extends StatelessWidget { Widget build(BuildContext context) { return AbsorbPointer( absorbing: !isClickable, - child: GestureDetector( - onTap: () => onSelected?.call(documentType?.id), - child: Text( - documentType?.toString() ?? "-", - style: (textStyle ?? Theme.of(context).textTheme.bodyMedium) - ?.copyWith(color: Theme.of(context).colorScheme.tertiary), - overflow: TextOverflow.ellipsis, - maxLines: 1, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(4), + onTap: () => onSelected?.call(documentType?.id), + child: Text( + documentType?.toString() ?? "-", + style: (textStyle ?? Theme.of(context).textTheme.bodyMedium) + ?.copyWith(color: Theme.of(context).colorScheme.tertiary), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ), ), ); diff --git a/lib/features/logging/view/app_logs_page.dart b/lib/features/logging/view/app_logs_page.dart index f8fef47..19e0d16 100644 --- a/lib/features/logging/view/app_logs_page.dart +++ b/lib/features/logging/view/app_logs_page.dart @@ -70,7 +70,9 @@ class _AppLogsPageState extends State { ), ), appBar: AppBar( - title: Text(S.of(context)!.appLogs(formattedDate)), + title: Text(S + .of(context)! + .appLogs(formattedDate)), //TODO: CHange to App-Logs in german actions: [ if (state is AppLogsStateLoaded) IconButton( diff --git a/lib/features/login/view/add_account_page.dart b/lib/features/login/view/add_account_page.dart index bf450be..07eab01 100644 --- a/lib/features/login/view/add_account_page.dart +++ b/lib/features/login/view/add_account_page.dart @@ -84,40 +84,42 @@ class _AddAccountPageState extends State { ), ), resizeToAvoidBottomInset: true, - body: FormBuilder( - key: _formKey, - child: ListView( - children: [ - ServerAddressFormField( - initialValue: widget.initialServerUrl, - onSubmit: (address) { - _updateReachability(address); - }, - ).padded(), - ClientCertificateFormField( - initialBytes: widget.initialClientCertificate?.bytes, - initialPassphrase: widget.initialClientCertificate?.passphrase, - onChanged: (_) => _updateReachability(), - ).padded(), - _buildStatusIndicator(), - if (_reachabilityStatus == ReachabilityStatus.reachable) ...[ - UserCredentialsFormField( - formKey: _formKey, - initialUsername: widget.initialUsername, - initialPassword: widget.initialPassword, - onFieldsSubmitted: _onSubmit, - ), - Text( - S.of(context)!.loginRequiredPermissionsHint, - style: Theme.of(context).textTheme.bodySmall?.apply( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - ), - ).padded(16), - ] - ], + body: AutofillGroup( + child: FormBuilder( + key: _formKey, + child: ListView( + children: [ + ServerAddressFormField( + initialValue: widget.initialServerUrl, + onSubmit: (address) { + _updateReachability(address); + }, + ).padded(), + ClientCertificateFormField( + initialBytes: widget.initialClientCertificate?.bytes, + initialPassphrase: widget.initialClientCertificate?.passphrase, + onChanged: (_) => _updateReachability(), + ).padded(), + _buildStatusIndicator(), + if (_reachabilityStatus == ReachabilityStatus.reachable) ...[ + UserCredentialsFormField( + formKey: _formKey, + initialUsername: widget.initialUsername, + initialPassword: widget.initialPassword, + onFieldsSubmitted: _onSubmit, + ), + Text( + S.of(context)!.loginRequiredPermissionsHint, + style: Theme.of(context).textTheme.bodySmall?.apply( + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.6), + ), + ).padded(16), + ] + ], + ), ), ), ); diff --git a/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart b/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart index e96d7d6..2f55bdc 100644 --- a/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart @@ -41,65 +41,62 @@ class _UserCredentialsFormFieldState extends State { username: widget.initialUsername, ), name: UserCredentialsFormField.fkCredentials, - builder: (field) => AutofillGroup( - child: Column( - children: [ - TextFormField( - key: const ValueKey('login-username'), - focusNode: _usernameFocusNode, - textCapitalization: TextCapitalization.none, - textInputAction: TextInputAction.next, - onFieldSubmitted: (value) { - _passwordFocusNode.requestFocus(); - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - autocorrect: false, - onChanged: (username) => field.didChange( - field.value?.copyWith(username: username) ?? - LoginFormCredentials(username: username), - ), - validator: (value) { - if (value?.trim().isEmpty ?? true) { - return S.of(context)!.usernameMustNotBeEmpty; - } - final serverAddress = widget.formKey.currentState! - .getRawValue( - ServerAddressFormField.fkServerAddress); - if (serverAddress != null) { - final userExists = Hive.localUserAccountBox.values - .map((e) => e.id) - .contains('$value@$serverAddress'); - if (userExists) { - return S.of(context)!.userAlreadyExists; - } - } - return null; - }, - autofillHints: const [AutofillHints.username], - decoration: InputDecoration( - label: Text(S.of(context)!.username), - ), + builder: (field) => Column( + children: [ + TextFormField( + key: const ValueKey('login-username'), + focusNode: _usernameFocusNode, + textCapitalization: TextCapitalization.none, + textInputAction: TextInputAction.next, + onFieldSubmitted: (value) { + _passwordFocusNode.requestFocus(); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + autocorrect: false, + onChanged: (username) => field.didChange( + field.value?.copyWith(username: username) ?? + LoginFormCredentials(username: username), ), - ObscuredInputTextFormField( - key: const ValueKey('login-password'), - focusNode: _passwordFocusNode, - label: S.of(context)!.password, - onChanged: (password) => field.didChange( - field.value?.copyWith(password: password) ?? - LoginFormCredentials(password: password), - ), - onFieldSubmitted: (_) { - widget.onFieldsSubmitted(); - }, - validator: (value) { - if (value?.trim().isEmpty ?? true) { - return S.of(context)!.passwordMustNotBeEmpty; + validator: (value) { + if (value?.trim().isEmpty ?? true) { + return S.of(context)!.usernameMustNotBeEmpty; + } + final serverAddress = widget.formKey.currentState! + .getRawValue(ServerAddressFormField.fkServerAddress); + if (serverAddress != null) { + final userExists = Hive.localUserAccountBox.values + .map((e) => e.id) + .contains('$value@$serverAddress'); + if (userExists) { + return S.of(context)!.userAlreadyExists; } - return null; - }, + } + return null; + }, + autofillHints: const [AutofillHints.username], + decoration: InputDecoration( + label: Text(S.of(context)!.username), ), - ].map((child) => child.padded()).toList(), - ), + ), + ObscuredInputTextFormField( + key: const ValueKey('login-password'), + focusNode: _passwordFocusNode, + label: S.of(context)!.password, + onChanged: (password) => field.didChange( + field.value?.copyWith(password: password) ?? + LoginFormCredentials(password: password), + ), + onFieldSubmitted: (_) { + widget.onFieldsSubmitted(); + }, + validator: (value) { + if (value?.trim().isEmpty ?? true) { + return S.of(context)!.passwordMustNotBeEmpty; + } + return null; + }, + ), + ].map((child) => child.padded()).toList(), ), ); } diff --git a/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart b/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart index 3c9cd67..8377544 100644 --- a/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart +++ b/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; @@ -21,11 +22,12 @@ mixin DocumentPagingBlocMixin Future loadMore() async { final hasConnection = await connectivityStatusService.isConnectedToInternet(); - if (state.isLastPageLoaded || !hasConnection) { + if (state.isLastPageLoaded || !hasConnection || state.isLoading) { return; } emit(state.copyWithPaged(isLoading: true)); final newFilter = state.filter.copyWith(page: state.filter.page + 1); + debugPrint("Fetching page ${newFilter.page}"); try { final result = await api.findAll(newFilter); emit( @@ -217,7 +219,6 @@ mixin DocumentPagingBlocMixin } } - @override Future close() { notifier.removeListener(this); diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index 5be033c..9f01e07 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/app_logs_tile.dart'; import 'package:paperless_mobile/features/settings/view/widgets/biometric_authentication_setting.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/changelogs_tile.dart'; import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_settings.dart'; import 'package:paperless_mobile/features/settings/view/widgets/color_scheme_option_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/default_download_file_type_setting.dart'; @@ -37,6 +39,9 @@ class SettingsPage extends StatelessWidget { const SkipDocumentPreprationOnShareSetting(), _buildSectionHeader(context, S.of(context)!.storage), const ClearCacheSetting(), + _buildSectionHeader(context, S.of(context)!.misc), + const AppLogsTile(), + const ChangelogsTile(), ], ), bottomNavigationBar: UserAccountBuilder( diff --git a/lib/features/settings/view/widgets/app_logs_tile.dart b/lib/features/settings/view/widgets/app_logs_tile.dart new file mode 100644 index 0000000..aee7d55 --- /dev/null +++ b/lib/features/settings/view/widgets/app_logs_tile.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routes/typed/top_level/app_logs_route.dart'; + +class AppLogsTile extends StatelessWidget { + const AppLogsTile({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.subject), + title: Text(S.of(context)!.appLogs('')), + onTap: () { + AppLogsRoute().push(context); + }, + ); + } +} diff --git a/lib/features/settings/view/widgets/changelogs_tile.dart b/lib/features/settings/view/widgets/changelogs_tile.dart new file mode 100644 index 0000000..747b530 --- /dev/null +++ b/lib/features/settings/view/widgets/changelogs_tile.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routes/typed/top_level/changelog_route.dart'; + +class ChangelogsTile extends StatelessWidget { + const ChangelogsTile({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.history), + title: Text(S.of(context)!.changelog), + onTap: () { + ChangelogRoute().push(context); + }, + ); + } +} diff --git a/lib/features/settings/view/widgets/language_selection_setting.dart b/lib/features/settings/view/widgets/language_selection_setting.dart index 1a1e3cc..cb18576 100644 --- a/lib/features/settings/view/widgets/language_selection_setting.dart +++ b/lib/features/settings/view/widgets/language_selection_setting.dart @@ -21,7 +21,7 @@ class _LanguageSelectionSettingState extends State { 'cs': LanguageOption('Česky', true), 'tr': LanguageOption('Türkçe', true), 'pl': LanguageOption('Polska', true), - 'ca': LanguageOption('Catalan', true), + 'ca': LanguageOption('Català', true), 'ru': LanguageOption('Русский', true), }; diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart index 4ec4653..563aeba 100644 --- a/lib/features/similar_documents/cubit/similar_documents_cubit.dart +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -13,6 +13,7 @@ class SimilarDocumentsCubit extends Cubit final int documentId; @override final ConnectivityStatusService connectivityStatusService; + @override final PaperlessDocumentsApi api; @@ -33,19 +34,9 @@ class SimilarDocumentsCubit extends Cubit onDeleted: remove, onUpdated: replace, ); - _labelRepository.addListener( - this, - onChanged: (labels) { - emit(state.copyWith( - correspondents: labels.correspondents, - documentTypes: labels.documentTypes, - tags: labels.tags, - storagePaths: labels.storagePaths, - )); - }, - ); } + @override Future initialize() async { if (!state.hasLoaded) { await updateFilter( diff --git a/lib/features/similar_documents/cubit/similar_documents_state.dart b/lib/features/similar_documents/cubit/similar_documents_state.dart index 503dbb5..e006fa6 100644 --- a/lib/features/similar_documents/cubit/similar_documents_state.dart +++ b/lib/features/similar_documents/cubit/similar_documents_state.dart @@ -1,20 +1,11 @@ part of 'similar_documents_cubit.dart'; class SimilarDocumentsState extends DocumentPagingState { - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; - const SimilarDocumentsState({ required super.filter, super.hasLoaded, super.isLoading, super.value, - this.correspondents = const {}, - this.documentTypes = const {}, - this.tags = const {}, - this.storagePaths = const {}, }); @override @@ -23,10 +14,6 @@ class SimilarDocumentsState extends DocumentPagingState { hasLoaded, isLoading, value, - correspondents, - documentTypes, - tags, - storagePaths, ]; @override @@ -49,20 +36,12 @@ class SimilarDocumentsState extends DocumentPagingState { bool? isLoading, List>? value, DocumentFilter? filter, - Map? correspondents, - Map? documentTypes, - Map? tags, - Map? storagePaths, }) { return SimilarDocumentsState( hasLoaded: hasLoaded ?? this.hasLoaded, isLoading: isLoading ?? this.isLoading, value: value ?? this.value, filter: filter ?? this.filter, - correspondents: correspondents ?? this.correspondents, - documentTypes: documentTypes ?? this.documentTypes, - tags: tags ?? this.tags, - storagePaths: storagePaths ?? this.storagePaths, ); } } diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index 7428f7b..f639029 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1001,13 +1001,23 @@ "@discardChangesWarning": { "description": "Warning message shown when the user tries to close a route without saving the changes." }, - "changelog": "Changelog", - "noLogsFoundOn": "No logs found on {date}.", - "logfileBottomReached": "You have reached the bottom of this logfile.", - "appLogs": "App logs {date}", - "saveLogsToFile": "Save logs to file", - "copyToClipboard": "Copy to clipboard", - "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", - "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "changelog": "Historial de canvis", + "noLogsFoundOn": "Sense logs trovats per {date}.", + "logfileBottomReached": "Final d'aquest arxiu de registres.", + "appLogs": "Logs d'aplicació {date}", + "saveLogsToFile": "Desar registres a arxiu", + "copyToClipboard": "Copia al porta-retalls", + "couldNotLoadLogfileFrom": "No es pot carregar log desde {date}.", + "loadingLogsFrom": "Carregant registres des de {date}...", + "clearLogs": "Netejar registres des de {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index e151c4d..19be896 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "Copy to clipboard", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index d902965..7b7345b 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "In Zwischenablage kopieren", "couldNotLoadLogfileFrom": "Logs vom {date} konnten nicht geladen werden.", "loadingLogsFrom": "Lade Logs vom {date}...", - "clearLogs": "Logs vom {date} leeren" + "clearLogs": "Logs vom {date} leeren", + "showPdf": "PDF anzeigen", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "PDF ausblenden", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Sonstige", + "loggingOut": "Abmelden..." } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2bfc980..3dcfffe 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "Copy to clipboard", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 61e2c55..7e1ce9e 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -984,7 +984,7 @@ "@authenticatingDots": { "description": "Message shown when the app is authenticating the user" }, - "persistingUserInformation": "Manteniendo información del usuario...", + "persistingUserInformation": "Preservando información del usuario...", "fetchingUserInformation": "Obteniendo información del usuario...", "@fetchingUserInformation": { "description": "Message shown when the app loads user data from the server" @@ -993,21 +993,31 @@ "@restoringSession": { "description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in" }, - "documentsAssigned": "{count, plural, zero{No documents} one{1 document} other{{count} documents}}", + "documentsAssigned": "{count, plural, zero{Sin documentos} one{1 documento} other{{count} documentos}}", "@documentsAssigned": { "description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield." }, - "discardChangesWarning": "You have unsaved changes. By continuing, all changes will be lost. Do you want to discard these changes?", + "discardChangesWarning": "Tienes cambios sin guardar. Si continúa, se perderán todos los cambios. ¿Quiere descartar estos cambios?", "@discardChangesWarning": { "description": "Warning message shown when the user tries to close a route without saving the changes." }, "changelog": "Changelog", - "noLogsFoundOn": "No logs found on {date}.", - "logfileBottomReached": "You have reached the bottom of this logfile.", - "appLogs": "App logs {date}", - "saveLogsToFile": "Save logs to file", - "copyToClipboard": "Copy to clipboard", - "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", - "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "noLogsFoundOn": "No se encontraron registros en {date}.", + "logfileBottomReached": "Has alcanzado el final del archivo de registro.", + "appLogs": "Registros de la aplicación {date}", + "saveLogsToFile": "Guardar registros en un archivo", + "copyToClipboard": "Copiar al portapapeles", + "couldNotLoadLogfileFrom": "No se pudo cargar el archivo de registro desde {date}.", + "loadingLogsFrom": "Cargando registros desde {date}...", + "clearLogs": "Limpiar registros desde {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index c605519..c33ce6b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "Copy to clipboard", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Sonstige", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index b9589fa..5f1256a 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "Copy to clipboard", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b6db6c0..a47d43b 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "Copy to clipboard", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 26935f8..2f69024 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1009,5 +1009,15 @@ "copyToClipboard": "Copy to clipboard", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}" + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out..." } \ No newline at end of file diff --git a/lib/routes/typed/top_level/logging_out_route.dart b/lib/routes/typed/top_level/logging_out_route.dart index 55378f6..d2151cd 100644 --- a/lib/routes/typed/top_level/logging_out_route.dart +++ b/lib/routes/typed/top_level/logging_out_route.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/routes.dart'; @@ -15,10 +16,10 @@ class LoggingOutRoute extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( + return NoTransitionPage( child: Scaffold( body: Center( - child: Text("Logging out..."), //TODO: INTL + child: Text(S.of(context)!.loggingOut), ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 04e7bc0..28cd598 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.0.6+53 +version: 3.1.0+54 environment: sdk: ">=3.0.0 <4.0.0"