feat: Update translations, add pdf view to document edit page

This commit is contained in:
Anton Stubenbord
2023-10-24 15:37:43 +02:00
parent 652abb6945
commit cb4839f5a3
40 changed files with 804 additions and 609 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -38,6 +38,8 @@ class DioHttpErrorInterceptor extends Interceptor {
const PaperlessApiException(ErrorCode.missingClientCertificate), const PaperlessApiException(ErrorCode.missingClientCertificate),
), ),
); );
} else {
handler.reject(err);
} }
} }
} }

View File

@@ -27,14 +27,15 @@ class DocumentChangedNotifier {
Object subscriber, { Object subscriber, {
DocumentChangedCallback? onUpdated, DocumentChangedCallback? onUpdated,
DocumentChangedCallback? onDeleted, DocumentChangedCallback? onDeleted,
Iterable<int>? ids,
}) { }) {
_subscribers.putIfAbsent( _subscribers.putIfAbsent(
subscriber, subscriber,
() => [ () => [
_updated.listen((value) { _updated.where((doc) => ids?.contains(doc.id) ?? true).listen((value) {
onUpdated?.call(value); onUpdated?.call(value);
}), }),
_deleted.listen((value) { _deleted.where((doc) => ids?.contains(doc.id) ?? true).listen((value) {
onDeleted?.call(value); onDeleted?.call(value);
}), }),
], ],

View File

@@ -53,6 +53,7 @@ class FormBuilderLocalizedDatePicker extends StatefulWidget {
final DateTime? initialValue; final DateTime? initialValue;
final DateTime firstDate; final DateTime firstDate;
final DateTime lastDate; final DateTime lastDate;
final FocusNode? focusNode;
/// If set to true, the field will not throw any validation errors when empty. /// If set to true, the field will not throw any validation errors when empty.
final bool allowUnset; final bool allowUnset;
@@ -67,6 +68,7 @@ class FormBuilderLocalizedDatePicker extends StatefulWidget {
required this.labelText, required this.labelText,
this.prefixIcon, this.prefixIcon,
this.allowUnset = false, this.allowUnset = false,
this.focusNode,
}); });
@override @override
@@ -100,8 +102,11 @@ class _FormBuilderLocalizedDatePickerState
final initialText = widget.initialValue != null final initialText = widget.initialValue != null
? DateFormat(formatString).format(widget.initialValue!) ? DateFormat(formatString).format(widget.initialValue!)
: null; : null;
final defaultFocusNode = FocusNode(debugLabel: formatString);
final focusNode =
i == 0 ? (widget.focusNode ?? defaultFocusNode) : defaultFocusNode;
final controls = _NeighbourAwareDateInputSegmentControls( final controls = _NeighbourAwareDateInputSegmentControls(
node: FocusNode(debugLabel: formatString), node: focusNode,
controller: TextEditingController(text: initialText), controller: TextEditingController(text: initialText),
format: formatString, format: formatString,
position: i, position: i,

View File

@@ -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( ListTile(
dense: true, dense: true,
leading: const Icon(Icons.bug_report_outlined), leading: const Icon(Icons.bug_report_outlined),
@@ -179,14 +171,6 @@ class AppDrawer extends StatelessWidget {
.fade(duration: 1.seconds, begin: 1, end: 0.3); .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( ListTile(
dense: true, dense: true,
leading: const Icon(Icons.settings_outlined), leading: const Icon(Icons.settings_outlined),

View File

@@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget {
} }
const _versionNumbers = { const _versionNumbers = {
"54": "3.0.7",
"53": "3.0.6", "53": "3.0.6",
"52": "3.0.5", "52": "3.0.5",
"51": "3.0.4", "51": "3.0.4",

View File

@@ -31,14 +31,13 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
this._notificationService, { this._notificationService, {
required this.id, required this.id,
}) : super(const DocumentDetailsInitial()) { }) : super(const DocumentDetailsInitial()) {
_notifier.addListener(this, onUpdated: (document) { _notifier.addListener(
if (state is DocumentDetailsLoaded) { this,
final currentState = state as DocumentDetailsLoaded; onUpdated: (document) {
if (document.id == currentState.document.id) { replace(document);
replace(document); },
} ids: [id],
} );
});
} }
Future<void> initialize() async { Future<void> initialize() async {

View File

@@ -22,11 +22,13 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
required DocumentModel document, required DocumentModel document,
}) : _initialDocument = document, }) : _initialDocument = document,
super(DocumentEditState(document: document)) { super(DocumentEditState(document: document)) {
_notifier.addListener(this, onUpdated: (doc) { _notifier.addListener(
if (doc.id == document.id) { this,
onUpdated: (doc) {
emit(state.copyWith(document: doc)); emit(state.copyWith(document: doc));
} },
}); ids: [document.id],
);
} }
Future<void> updateDocument(DocumentModel document) async { Future<void> updateDocument(DocumentModel document) async {

View File

@@ -9,13 +9,13 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/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/widgets/form_builder_fields/form_builder_localized_date_picker.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.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/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/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -32,7 +32,8 @@ class DocumentEditPage extends StatefulWidget {
State<DocumentEditPage> createState() => _DocumentEditPageState(); State<DocumentEditPage> createState() => _DocumentEditPageState();
} }
class _DocumentEditPageState extends State<DocumentEditPage> { class _DocumentEditPageState extends State<DocumentEditPage>
with SingleTickerProviderStateMixin {
static const fkTitle = "title"; static const fkTitle = "title";
static const fkCorrespondent = "correspondent"; static const fkCorrespondent = "correspondent";
static const fkTags = "tags"; static const fkTags = "tags";
@@ -43,6 +44,23 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
bool _isShowingPdf = false;
late final AnimationController _animationController;
late final Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_animation =
CurvedAnimation(parent: _animationController, curve: Curves.easeInCubic)
.drive(Tween<double>(begin: 0, end: 1));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser; final currentUser = context.watch<LocalUserAccount>().paperlessUser;
@@ -75,197 +93,228 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
doc.created != createdAt || doc.created != createdAt ||
(doc.content != content && isContentTouched); (doc.content != content && isContentTouched);
}, },
child: DefaultTabController( child: FormBuilder(
length: 2, key: _formKey,
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false, appBar: AppBar(
floatingActionButton: FloatingActionButton.extended( title: Text(S.of(context)!.editDocument),
heroTag: "fab_document_edit", actions: [
onPressed: () => _onSubmit(state.document), IconButton(
icon: const Icon(Icons.save), tooltip: _isShowingPdf
label: Text(S.of(context)!.saveChanges), ? S.of(context)!.hidePdf
), : S.of(context)!.showPdf,
appBar: AppBar( padding: EdgeInsets.all(12),
title: Text(S.of(context)!.editDocument), icon: AnimatedCrossFade(
bottom: TabBar( duration: _animationController.duration!,
tabs: [ reverseDuration: _animationController.reverseDuration,
Tab(text: S.of(context)!.overview), crossFadeState: _isShowingPdf
Tab(text: S.of(context)!.content) ? CrossFadeState.showFirst
], : CrossFadeState.showSecond,
), firstChild: Icon(Icons.visibility_off_outlined),
), secondChild: Icon(Icons.visibility_outlined),
extendBody: true, ),
body: Padding( onPressed: () {
padding: EdgeInsets.only( if (_isShowingPdf) {
bottom: MediaQuery.of(context).viewInsets.bottom, setState(() {
top: 8, _isShowingPdf = false;
left: 8, });
right: 8, _animationController.reverse();
), } else {
child: FormBuilder( setState(() {
key: _formKey, _isShowingPdf = true;
child: TabBarView( });
children: [ _animationController.forward();
ListView( }
children: [ },
_buildTitleFormField(state.document.title).padded(), )
_buildCreatedAtFormField( ],
state.document.created, ),
filteredSuggestions, body: Stack(
).padded(), children: [
// Correspondent form field DefaultTabController(
if (currentUser.canViewCorrespondents) length: 2,
Column( child: Scaffold(
children: [ resizeToAvoidBottomInset: true,
LabelFormField<Correspondent>( floatingActionButton: !_isShowingPdf
showAnyAssignedOption: false, ? FloatingActionButton.extended(
showNotAssignedOption: false, heroTag: "fab_document_edit",
onAddLabel: (currentInput) => onPressed: () => _onSubmit(state.document),
CreateLabelRoute( icon: const Icon(Icons.save),
LabelType.correspondent, label: Text(S.of(context)!.saveChanges),
name: currentInput, )
).push<Correspondent>(context), : null,
addLabelText: appBar: TabBar(
S.of(context)!.addCorrespondent, tabs: [
labelText: S.of(context)!.correspondent, Tab(text: S.of(context)!.overview),
options: context Tab(text: S.of(context)!.content),
.watch<LabelRepository>() ],
.state ),
.correspondents, extendBody: true,
initialValue: state body: _buildEditForm(
.document.correspondent != context,
null state,
? SetIdQueryParameter( filteredSuggestions,
id: state.document.correspondent!) currentUser,
: 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<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
onAddLabel: (currentInput) =>
CreateLabelRoute(
LabelType.documentType,
name: currentInput,
).push<DocumentType>(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<LabelRepository>()
.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<StoragePath>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
onAddLabel: (currentInput) =>
CreateLabelRoute(
LabelType.storagePath,
name: currentInput,
).push<StoragePath>(context),
canCreateNewLabel:
currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: context
.watch<LabelRepository>()
.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<LabelRepository>().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),
],
),
),
],
), ),
), ),
)), AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
alignment: Alignment.bottomLeft,
scale: _animation.value,
child: DocumentView(
showAppBar: false,
showControls: false,
documentBytes: context
.read<PaperlessDocumentsApi>()
.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<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
onAddLabel: (currentInput) => CreateLabelRoute(
LabelType.correspondent,
name: currentInput,
).push<Correspondent>(context),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options:
context.watch<LabelRepository>().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<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
onAddLabel: (currentInput) => CreateLabelRoute(
LabelType.documentType,
name: currentInput,
).push<DocumentType>(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<LabelRepository>().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<StoragePath>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
onAddLabel: (currentInput) => CreateLabelRoute(
LabelType.storagePath,
name: currentInput,
).push<StoragePath>(context),
canCreateNewLabel: currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options:
context.watch<LabelRepository>().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<LabelRepository>().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, String? title,
int? correspondent, int? correspondent,

View File

@@ -5,9 +5,13 @@ import 'package:flutter_pdfview/flutter_pdfview.dart';
class DocumentView extends StatefulWidget { class DocumentView extends StatefulWidget {
final Future<Uint8List> documentBytes; final Future<Uint8List> documentBytes;
final String? title; final String? title;
final bool showAppBar;
final bool showControls;
const DocumentView({ const DocumentView({
Key? key, Key? key,
required this.documentBytes, required this.documentBytes,
this.showAppBar = true,
this.showControls = true,
this.title, this.title,
}) : super(key: key); }) : super(key: key);
@@ -27,43 +31,47 @@ class _DocumentViewState extends State<DocumentView> {
final canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!; final canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!;
final canGoToPreviousPage = isInitialized && _currentPage! > 0; final canGoToPreviousPage = isInitialized && _currentPage! > 0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: widget.showAppBar
title: widget.title != null ? Text(widget.title!) : null, ? AppBar(
), title: widget.title != null ? Text(widget.title!) : null,
bottomNavigationBar: BottomAppBar( )
child: Row( : null,
children: [ bottomNavigationBar: widget.showControls
Flexible( ? BottomAppBar(
child: Row( child: Row(
children: [ children: [
IconButton.filled( Flexible(
onPressed: canGoToPreviousPage child: Row(
? () { children: [
_controller?.setPage(_currentPage! - 1); IconButton.filled(
} onPressed: canGoToPreviousPage
: null, ? () {
icon: const Icon(Icons.arrow_left), _controller?.setPage(_currentPage! - 1);
), }
const SizedBox(width: 16), : null,
IconButton.filled( icon: const Icon(Icons.arrow_left),
onPressed: canGoToNextPage ),
? () { const SizedBox(width: 16),
_controller?.setPage(_currentPage! + 1); IconButton.filled(
} onPressed: canGoToNextPage
: null, ? () {
icon: const Icon(Icons.arrow_right), _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) : null,
Text(
"${_currentPage! + 1}/$_totalPages",
style: Theme.of(context).textTheme.labelLarge,
),
],
),
),
body: FutureBuilder( body: FutureBuilder(
future: widget.documentBytes, future: widget.documentBytes,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -93,12 +101,7 @@ class _DocumentViewState extends State<DocumentView> {
onViewCreated: (controller) { onViewCreated: (controller) {
_controller = controller; _controller = controller;
}, },
onError: (error) {
print(error.toString());
},
onPageError: (page, error) {
print('$page: ${error.toString()}');
},
); );
}), }),
); );

View File

@@ -110,7 +110,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
} }
void _scrollExtentChangedListener() { void _scrollExtentChangedListener() {
const threshold = 400; const threshold = kToolbarHeight * 2;
final offset = final offset =
_nestedScrollViewKey.currentState!.innerController.position.pixels; _nestedScrollViewKey.currentState!.innerController.position.pixels;
if (offset < threshold && _showExtendedFab == false) { if (offset < threshold && _showExtendedFab == false) {
@@ -429,6 +429,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
); );
}, },
), ),
const SliverToBoxAdapter(
child: SizedBox(height: 96),
)
], ],
), ),
), ),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.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/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_detailed_item.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_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, crossAxisCount: 2,
mainAxisSpacing: 4, mainAxisSpacing: 4,
crossAxisSpacing: 4, crossAxisSpacing: 4,
mainAxisExtent: 356, mainAxisExtent: 324,
), ),
itemCount: documents.length, itemCount: documents.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@@ -176,7 +177,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected, onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected, onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation, enableHeroAnimation: enableHeroAnimation,
); ).paddedSymmetrically(horizontal: 4);
}, },
); );
} }

View File

@@ -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<LabelRepository>()
.state
.documentTypes[document.documentType]!
.name,
style: subtitleStyle,
),
),
),
),
]
: null,
),
);
}
}

View File

@@ -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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.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/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.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'; 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: RichText( child: DateAndDocumentTypeLabelWidget(
maxLines: 1, document: document,
overflow: TextOverflow.ellipsis, onDocumentTypeSelected: onDocumentTypeSelected,
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,
),
],
],
),
), ),
), ),
if (document.archiveSerialNumber != null) if (document.archiveSerialNumber != null)
@@ -143,30 +134,7 @@ class DocumentDetailedItem extends DocumentItem {
?.apply(color: Theme.of(context).hintColor), ?.apply(color: Theme.of(context).hintColor),
), ),
], ],
).paddedLTRB(8, 8, 8, 4), ).paddedLTRB(8, 4, 8, 8),
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),
if (highlights != null) if (highlights != null)
Html( Html(
data: '<p>${highlights!}</p>', data: '<p>${highlights!}</p>',

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.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/documents/view/widgets/items/document_item.dart';
@@ -29,111 +30,133 @@ class DocumentGridItem extends DocumentItem {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var currentUser = context.watch<LocalUserAccount>().paperlessUser; var currentUser = context.watch<LocalUserAccount>().paperlessUser;
return Padding( return Stack(
padding: const EdgeInsets.all(8.0), children: [
child: Card( Card(
elevation: 1.0, elevation: 1.0,
color: isSelected color: isSelected
? Theme.of(context).colorScheme.inversePrimary ? Theme.of(context).colorScheme.inversePrimary
: Theme.of(context).cardColor, : Theme.of(context).cardColor,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: _onTap, onTap: _onTap,
onLongPress: onSelected != null ? () => onSelected!(document) : null, onLongPress:
child: Column( onSelected != null ? () => onSelected!(document) : null,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
AspectRatio( children: [
aspectRatio: 1, ClipRRect(
child: Stack( borderRadius: BorderRadius.circular(12),
children: [ child: AspectRatio(
Positioned.fill( aspectRatio: 1,
child: DocumentPreview( child: Stack(
documentId: document.id, children: [
borderRadius: 12.0, Positioned.fill(
enableHero: enableHeroAnimation, child: DocumentPreview(
), documentId: document.id,
), borderRadius: 12.0,
Align( enableHero: enableHeroAnimation,
alignment: Alignment.bottomLeft,
child: SizedBox(
height: 48,
child: NotificationListener<ScrollNotification>(
// 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<LabelRepository>()
.state
.tags[e]!)
.toList(),
onTagSelected: onTagSelected,
),
const SliverToBoxAdapter(
child: SizedBox(width: 8),
),
],
), ),
), ),
), Align(
alignment: Alignment.bottomLeft,
child: SizedBox(
height: kMinInteractiveDimension,
child: NotificationListener<ScrollNotification>(
// 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<LabelRepository>()
.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<LabelRepository>()
.state
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
if (currentUser.canViewDocumentTypes)
DocumentTypeWidget(
documentType: context
.watch<LabelRepository>()
.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<LabelRepository>()
.state
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
if (currentUser.canViewDocumentTypes)
DocumentTypeWidget(
documentType: context
.watch<LabelRepository>()
.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,
),
)
],
),
],
),
),
),
],
),
), ),
), ),
), ],
); );
} }

View File

@@ -1,7 +1,9 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.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.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/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.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'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
@@ -31,6 +33,7 @@ class DocumentListItem extends DocumentItem {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state; final labels = context.watch<LabelRepository>().state;
return ListTile( return ListTile(
tileColor: backgroundColor, tileColor: backgroundColor,
dense: true, dense: true,
@@ -75,35 +78,11 @@ class DocumentListItem extends DocumentItem {
), ),
], ],
), ),
subtitle: IntrinsicWidth( subtitle: Padding(
child: Padding( padding: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(vertical: 4), child: DateAndDocumentTypeLabelWidget(
child: RichText( document: document,
maxLines: 1, onDocumentTypeSelected: onDocumentTypeSelected,
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,
),
),
), ),
), ),
isThreeLine: document.tags.isNotEmpty, isThreeLine: document.tags.isNotEmpty,

View File

@@ -21,15 +21,19 @@ class CorrespondentWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AbsorbPointer( return AbsorbPointer(
absorbing: !isClickable, absorbing: !isClickable,
child: GestureDetector( child: Material(
onTap: () => onSelected?.call(correspondent?.id), color: Colors.transparent,
child: Text( child: InkWell(
correspondent?.name ?? "-", borderRadius: BorderRadius.circular(4),
maxLines: 1, onTap: () => onSelected?.call(correspondent?.id),
overflow: TextOverflow.ellipsis, child: Text(
style: correspondent?.name ?? "-",
(textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith( maxLines: 1,
color: textColor ?? Theme.of(context).colorScheme.primary, overflow: TextOverflow.ellipsis,
style:
(textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
), ),
), ),
), ),

View File

@@ -18,14 +18,18 @@ class DocumentTypeWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AbsorbPointer( return AbsorbPointer(
absorbing: !isClickable, absorbing: !isClickable,
child: GestureDetector( child: Material(
onTap: () => onSelected?.call(documentType?.id), color: Colors.transparent,
child: Text( child: InkWell(
documentType?.toString() ?? "-", borderRadius: BorderRadius.circular(4),
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium) onTap: () => onSelected?.call(documentType?.id),
?.copyWith(color: Theme.of(context).colorScheme.tertiary), child: Text(
overflow: TextOverflow.ellipsis, documentType?.toString() ?? "-",
maxLines: 1, style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
), ),
), ),
); );

View File

@@ -70,7 +70,9 @@ class _AppLogsPageState extends State<AppLogsPage> {
), ),
), ),
appBar: AppBar( appBar: AppBar(
title: Text(S.of(context)!.appLogs(formattedDate)), title: Text(S
.of(context)!
.appLogs(formattedDate)), //TODO: CHange to App-Logs in german
actions: [ actions: [
if (state is AppLogsStateLoaded) if (state is AppLogsStateLoaded)
IconButton( IconButton(

View File

@@ -84,40 +84,42 @@ class _AddAccountPageState extends State<AddAccountPage> {
), ),
), ),
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
body: FormBuilder( body: AutofillGroup(
key: _formKey, child: FormBuilder(
child: ListView( key: _formKey,
children: [ child: ListView(
ServerAddressFormField( children: [
initialValue: widget.initialServerUrl, ServerAddressFormField(
onSubmit: (address) { initialValue: widget.initialServerUrl,
_updateReachability(address); onSubmit: (address) {
}, _updateReachability(address);
).padded(), },
ClientCertificateFormField( ).padded(),
initialBytes: widget.initialClientCertificate?.bytes, ClientCertificateFormField(
initialPassphrase: widget.initialClientCertificate?.passphrase, initialBytes: widget.initialClientCertificate?.bytes,
onChanged: (_) => _updateReachability(), initialPassphrase: widget.initialClientCertificate?.passphrase,
).padded(), onChanged: (_) => _updateReachability(),
_buildStatusIndicator(), ).padded(),
if (_reachabilityStatus == ReachabilityStatus.reachable) ...[ _buildStatusIndicator(),
UserCredentialsFormField( if (_reachabilityStatus == ReachabilityStatus.reachable) ...[
formKey: _formKey, UserCredentialsFormField(
initialUsername: widget.initialUsername, formKey: _formKey,
initialPassword: widget.initialPassword, initialUsername: widget.initialUsername,
onFieldsSubmitted: _onSubmit, initialPassword: widget.initialPassword,
), onFieldsSubmitted: _onSubmit,
Text( ),
S.of(context)!.loginRequiredPermissionsHint, Text(
style: Theme.of(context).textTheme.bodySmall?.apply( S.of(context)!.loginRequiredPermissionsHint,
color: Theme.of(context) style: Theme.of(context).textTheme.bodySmall?.apply(
.colorScheme color: Theme.of(context)
.onBackground .colorScheme
.withOpacity(0.6), .onBackground
), .withOpacity(0.6),
).padded(16), ),
] ).padded(16),
], ]
],
),
), ),
), ),
); );

View File

@@ -41,65 +41,62 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
username: widget.initialUsername, username: widget.initialUsername,
), ),
name: UserCredentialsFormField.fkCredentials, name: UserCredentialsFormField.fkCredentials,
builder: (field) => AutofillGroup( builder: (field) => Column(
child: Column( children: [
children: [ TextFormField(
TextFormField( key: const ValueKey('login-username'),
key: const ValueKey('login-username'), focusNode: _usernameFocusNode,
focusNode: _usernameFocusNode, textCapitalization: TextCapitalization.none,
textCapitalization: TextCapitalization.none, textInputAction: TextInputAction.next,
textInputAction: TextInputAction.next, onFieldSubmitted: (value) {
onFieldSubmitted: (value) { _passwordFocusNode.requestFocus();
_passwordFocusNode.requestFocus(); },
}, autovalidateMode: AutovalidateMode.onUserInteraction,
autovalidateMode: AutovalidateMode.onUserInteraction, autocorrect: false,
autocorrect: false, onChanged: (username) => field.didChange(
onChanged: (username) => field.didChange( field.value?.copyWith(username: username) ??
field.value?.copyWith(username: username) ?? LoginFormCredentials(username: username),
LoginFormCredentials(username: username),
),
validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.usernameMustNotBeEmpty;
}
final serverAddress = widget.formKey.currentState!
.getRawValue<String>(
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),
),
), ),
ObscuredInputTextFormField( validator: (value) {
key: const ValueKey('login-password'), if (value?.trim().isEmpty ?? true) {
focusNode: _passwordFocusNode, return S.of(context)!.usernameMustNotBeEmpty;
label: S.of(context)!.password, }
onChanged: (password) => field.didChange( final serverAddress = widget.formKey.currentState!
field.value?.copyWith(password: password) ?? .getRawValue<String>(ServerAddressFormField.fkServerAddress);
LoginFormCredentials(password: password), if (serverAddress != null) {
), final userExists = Hive.localUserAccountBox.values
onFieldSubmitted: (_) { .map((e) => e.id)
widget.onFieldsSubmitted(); .contains('$value@$serverAddress');
}, if (userExists) {
validator: (value) { return S.of(context)!.userAlreadyExists;
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.passwordMustNotBeEmpty;
} }
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(),
), ),
); );
} }

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
@@ -21,11 +22,12 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
Future<void> loadMore() async { Future<void> loadMore() async {
final hasConnection = final hasConnection =
await connectivityStatusService.isConnectedToInternet(); await connectivityStatusService.isConnectedToInternet();
if (state.isLastPageLoaded || !hasConnection) { if (state.isLastPageLoaded || !hasConnection || state.isLoading) {
return; return;
} }
emit(state.copyWithPaged(isLoading: true)); emit(state.copyWithPaged(isLoading: true));
final newFilter = state.filter.copyWith(page: state.filter.page + 1); final newFilter = state.filter.copyWith(page: state.filter.page + 1);
debugPrint("Fetching page ${newFilter.page}");
try { try {
final result = await api.findAll(newFilter); final result = await api.findAll(newFilter);
emit( emit(
@@ -217,7 +219,6 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
} }
} }
@override @override
Future<void> close() { Future<void> close() {
notifier.removeListener(this); notifier.removeListener(this);

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.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/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/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/color_scheme_option_setting.dart';
import 'package:paperless_mobile/features/settings/view/widgets/default_download_file_type_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(), const SkipDocumentPreprationOnShareSetting(),
_buildSectionHeader(context, S.of(context)!.storage), _buildSectionHeader(context, S.of(context)!.storage),
const ClearCacheSetting(), const ClearCacheSetting(),
_buildSectionHeader(context, S.of(context)!.misc),
const AppLogsTile(),
const ChangelogsTile(),
], ],
), ),
bottomNavigationBar: UserAccountBuilder( bottomNavigationBar: UserAccountBuilder(

View File

@@ -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);
},
);
}
}

View File

@@ -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);
},
);
}
}

View File

@@ -21,7 +21,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
'cs': LanguageOption('Česky', true), 'cs': LanguageOption('Česky', true),
'tr': LanguageOption('Türkçe', true), 'tr': LanguageOption('Türkçe', true),
'pl': LanguageOption('Polska', true), 'pl': LanguageOption('Polska', true),
'ca': LanguageOption('Catalan', true), 'ca': LanguageOption('Català', true),
'ru': LanguageOption('Русский', true), 'ru': LanguageOption('Русский', true),
}; };

View File

@@ -13,6 +13,7 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
final int documentId; final int documentId;
@override @override
final ConnectivityStatusService connectivityStatusService; final ConnectivityStatusService connectivityStatusService;
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
@@ -33,19 +34,9 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
onDeleted: remove, onDeleted: remove,
onUpdated: replace, onUpdated: replace,
); );
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
));
},
);
} }
@override
Future<void> initialize() async { Future<void> initialize() async {
if (!state.hasLoaded) { if (!state.hasLoaded) {
await updateFilter( await updateFilter(

View File

@@ -1,20 +1,11 @@
part of 'similar_documents_cubit.dart'; part of 'similar_documents_cubit.dart';
class SimilarDocumentsState extends DocumentPagingState { class SimilarDocumentsState extends DocumentPagingState {
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
const SimilarDocumentsState({ const SimilarDocumentsState({
required super.filter, required super.filter,
super.hasLoaded, super.hasLoaded,
super.isLoading, super.isLoading,
super.value, super.value,
this.correspondents = const {},
this.documentTypes = const {},
this.tags = const {},
this.storagePaths = const {},
}); });
@override @override
@@ -23,10 +14,6 @@ class SimilarDocumentsState extends DocumentPagingState {
hasLoaded, hasLoaded,
isLoading, isLoading,
value, value,
correspondents,
documentTypes,
tags,
storagePaths,
]; ];
@override @override
@@ -49,20 +36,12 @@ class SimilarDocumentsState extends DocumentPagingState {
bool? isLoading, bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value, List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter, DocumentFilter? filter,
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, Tag>? tags,
Map<int, StoragePath>? storagePaths,
}) { }) {
return SimilarDocumentsState( return SimilarDocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded, hasLoaded: hasLoaded ?? this.hasLoaded,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
value: value ?? this.value, value: value ?? this.value,
filter: filter ?? this.filter, filter: filter ?? this.filter,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
tags: tags ?? this.tags,
storagePaths: storagePaths ?? this.storagePaths,
); );
} }
} }

View File

@@ -1001,13 +1001,23 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "description": "Warning message shown when the user tries to close a route without saving the changes."
}, },
"changelog": "Changelog", "changelog": "Historial de canvis",
"noLogsFoundOn": "No logs found on {date}.", "noLogsFoundOn": "Sense logs trovats per {date}.",
"logfileBottomReached": "You have reached the bottom of this logfile.", "logfileBottomReached": "Final d'aquest arxiu de registres.",
"appLogs": "App logs {date}", "appLogs": "Logs d'aplicació {date}",
"saveLogsToFile": "Save logs to file", "saveLogsToFile": "Desar registres a arxiu",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copia al porta-retalls",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "No es pot carregar log desde {date}.",
"loadingLogsFrom": "Loading logs from {date}...", "loadingLogsFrom": "Carregant registres des de {date}...",
"clearLogs": "Clear logs from {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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
"loadingLogsFrom": "Loading logs 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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "In Zwischenablage kopieren", "copyToClipboard": "In Zwischenablage kopieren",
"couldNotLoadLogfileFrom": "Logs vom {date} konnten nicht geladen werden.", "couldNotLoadLogfileFrom": "Logs vom {date} konnten nicht geladen werden.",
"loadingLogsFrom": "Lade Logs vom {date}...", "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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
"loadingLogsFrom": "Loading logs 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..."
} }

View File

@@ -984,7 +984,7 @@
"@authenticatingDots": { "@authenticatingDots": {
"description": "Message shown when the app is authenticating the user" "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": "Obteniendo información del usuario...",
"@fetchingUserInformation": { "@fetchingUserInformation": {
"description": "Message shown when the app loads user data from the server" "description": "Message shown when the app loads user data from the server"
@@ -993,21 +993,31 @@
"@restoringSession": { "@restoringSession": {
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in" "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": { "@documentsAssigned": {
"description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield." "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": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "description": "Warning message shown when the user tries to close a route without saving the changes."
}, },
"changelog": "Changelog", "changelog": "Changelog",
"noLogsFoundOn": "No logs found on {date}.", "noLogsFoundOn": "No se encontraron registros en {date}.",
"logfileBottomReached": "You have reached the bottom of this logfile.", "logfileBottomReached": "Has alcanzado el final del archivo de registro.",
"appLogs": "App logs {date}", "appLogs": "Registros de la aplicación {date}",
"saveLogsToFile": "Save logs to file", "saveLogsToFile": "Guardar registros en un archivo",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copiar al portapapeles",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "No se pudo cargar el archivo de registro desde {date}.",
"loadingLogsFrom": "Loading logs from {date}...", "loadingLogsFrom": "Cargando registros desde {date}...",
"clearLogs": "Clear logs from {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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
"loadingLogsFrom": "Loading logs 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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
"loadingLogsFrom": "Loading logs 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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
"loadingLogsFrom": "Loading logs 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..."
} }

View File

@@ -1009,5 +1009,15 @@
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.", "couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
"loadingLogsFrom": "Loading logs 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..."
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.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/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart'; import 'package:paperless_mobile/routes/routes.dart';
@@ -15,10 +16,10 @@ class LoggingOutRoute extends GoRouteData {
@override @override
Page<void> buildPage(BuildContext context, GoRouterState state) { Page<void> buildPage(BuildContext context, GoRouterState state) {
return const NoTransitionPage( return NoTransitionPage(
child: Scaffold( child: Scaffold(
body: Center( body: Center(
child: Text("Logging out..."), //TODO: INTL child: Text(S.of(context)!.loggingOut),
), ),
), ),
); );

View File

@@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 3.0.6+53 version: 3.1.0+54
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"