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

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

View File

@@ -27,14 +27,15 @@ class DocumentChangedNotifier {
Object subscriber, {
DocumentChangedCallback? onUpdated,
DocumentChangedCallback? onDeleted,
Iterable<int>? 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);
}),
],

View File

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

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(
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),

View File

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

View File

@@ -31,14 +31,13 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
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<void> initialize() async {

View File

@@ -22,11 +22,13 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
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<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: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<DocumentEditPage> createState() => _DocumentEditPageState();
}
class _DocumentEditPageState extends State<DocumentEditPage> {
class _DocumentEditPageState extends State<DocumentEditPage>
with SingleTickerProviderStateMixin {
static const fkTitle = "title";
static const fkCorrespondent = "correspondent";
static const fkTags = "tags";
@@ -43,6 +44,23 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
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
Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
@@ -75,197 +93,228 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
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<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(),
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<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,
int? correspondent,

View File

@@ -5,9 +5,13 @@ import 'package:flutter_pdfview/flutter_pdfview.dart';
class DocumentView extends StatefulWidget {
final Future<Uint8List> 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<DocumentView> {
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<DocumentView> {
onViewCreated: (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() {
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<DocumentsPage> {
);
},
),
const SliverToBoxAdapter(
child: SizedBox(height: 96),
)
],
),
),

View File

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

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/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: '<p>${highlights!}</p>',

View File

@@ -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<LocalUserAccount>().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<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),
),
],
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<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: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<LabelRepository>().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,

View File

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

View File

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

View File

@@ -70,7 +70,9 @@ class _AppLogsPageState extends State<AppLogsPage> {
),
),
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(

View File

@@ -84,40 +84,42 @@ class _AddAccountPageState extends State<AddAccountPage> {
),
),
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),
]
],
),
),
),
);

View File

@@ -41,65 +41,62 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
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<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),
),
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<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;
},
}
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: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<State extends DocumentPagingState>
Future<void> 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<State extends DocumentPagingState>
}
}
@override
Future<void> close() {
notifier.removeListener(this);

View File

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

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),
'tr': LanguageOption('Türkçe', true),
'pl': LanguageOption('Polska', true),
'ca': LanguageOption('Catalan', true),
'ca': LanguageOption('Català', true),
'ru': LanguageOption('Русский', true),
};

View File

@@ -13,6 +13,7 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
final int documentId;
@override
final ConnectivityStatusService connectivityStatusService;
@override
final PaperlessDocumentsApi api;
@@ -33,19 +34,9 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
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<void> initialize() async {
if (!state.hasLoaded) {
await updateFilter(

View File

@@ -1,20 +1,11 @@
part of 'similar_documents_cubit.dart';
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({
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<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, Tag>? tags,
Map<int, StoragePath>? 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,
);
}
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

@@ -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..."
}

View File

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