feat: Migrations, new saved views interface

This commit is contained in:
Anton Stubenbord
2023-09-19 01:50:02 +02:00
parent 2e8144700f
commit f3560f00ea
31 changed files with 1745 additions and 376 deletions

View File

@@ -1,17 +1,11 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
class AppDrawer extends StatelessWidget {
@@ -61,27 +55,40 @@ class AppDrawer extends StatelessWidget {
),
ListTile(
dense: true,
leading: Padding(
padding: const EdgeInsets.only(left: 3),
child: SvgPicture.asset(
'assets/images/bmc-logo.svg',
width: 24,
height: 24,
),
leading: const Icon(Icons.favorite_outline),
title: Text(S.of(context)!.donate),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.favorite),
title: Text(S.of(context)!.donate),
content: Text(
S.of(context)!.donationDialogContent,
),
actions: const [
Text("~ Anton"),
],
),
);
},
),
ListTile(
dense: true,
leading: SvgPicture.asset(
"assets/images/github-mark.svg",
color: Theme.of(context).colorScheme.onBackground,
height: 24,
width: 24,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.donateCoffee),
const Icon(
Icons.open_in_new,
size: 16,
)
],
title: Text(S.of(context)!.sourceCode),
trailing: const Icon(
Icons.open_in_new,
size: 16,
),
onTap: () {
launchUrlString(
"https://www.buymeacoffee.com/astubenbord",
"https://github.com/astubenbord/paperless-mobile",
mode: LaunchMode.externalApplication,
);
},

View File

@@ -1,6 +1,7 @@
import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
@@ -13,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_changed_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_views_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
@@ -45,29 +47,13 @@ class _DocumentsPageState extends State<DocumentsPage>
with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle savedViewsHandle =
SliverOverlapAbsorberHandle();
late final TabController _tabController;
int _currentTab = 0;
bool get hasSelectedViewChanged {
final cubit = context.watch<DocumentsCubit>();
final savedViewCubit = context.watch<SavedViewCubit>();
final activeView = savedViewCubit.state.maybeMap(
loaded: (state) {
if (cubit.state.filter.selectedView != null) {
return state.savedViews[cubit.state.filter.selectedView!];
}
return null;
},
orElse: () => null,
);
final viewHasChanged = activeView != null &&
activeView.toDocumentFilter() != cubit.state.filter;
return viewHasChanged;
}
final _savedViewsExpansionController = ExpansionTileController();
@override
void initState() {
super.initState();
@@ -177,21 +163,14 @@ class _DocumentsPageState extends State<DocumentsPage>
animationType: b.BadgeAnimationType.fade,
badgeColor: Colors.red,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: (_currentTab == 0)
? FloatingActionButton(
heroTag: "fab_documents_page_filter",
child:
const Icon(Icons.filter_alt_outlined),
onPressed: _openDocumentFilter,
)
: FloatingActionButton(
heroTag: "fab_documents_page_filter",
child: const Icon(Icons.add),
onPressed: () =>
_onCreateSavedView(state.filter),
),
),
duration: const Duration(milliseconds: 250),
child: Builder(builder: (context) {
return FloatingActionButton(
heroTag: "fab_documents_page_filter",
child: const Icon(Icons.filter_alt_outlined),
onPressed: _openDocumentFilter,
);
})),
),
],
),
@@ -236,7 +215,10 @@ class _DocumentsPageState extends State<DocumentsPage>
SliverOverlapAbsorber(
handle: savedViewsHandle,
sliver: SliverPinnedHeader(
child: _buildViewActions(),
child: Material(
child: _buildViewActions(),
elevation: 4,
),
),
),
// SliverOverlapAbsorber(
@@ -288,70 +270,13 @@ class _DocumentsPageState extends State<DocumentsPage>
}
return false;
},
child: TabBarView(
controller: _tabController,
physics: context
.watch<DocumentsCubit>()
.state
.selection
.isNotEmpty
? const NeverScrollableScrollPhysics()
: null,
children: [
Builder(
builder: (context) {
return _buildDocumentsTab(
connectivityState,
context,
);
},
),
if (context
.watch<LocalUserAccount>()
.paperlessUser
.canViewSavedViews)
Builder(
builder: (context) {
return _buildSavedViewsTab(
connectivityState,
context,
);
},
),
],
),
),
),
AnimatedOpacity(
opacity: hasSelectedViewChanged ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: const EdgeInsets.only(bottom: 24),
child: Material(
borderRadius: BorderRadius.circular(24),
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.9),
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
onTap: () {},
child: Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Text(
"Update selected view",
style: Theme.of(context).textTheme.labelLarge,
),
),
),
),
child: _buildDocumentsTab(
connectivityState,
context,
),
),
),
_buildSavedViewChangedIndicator(),
],
),
),
@@ -362,29 +287,82 @@ class _DocumentsPageState extends State<DocumentsPage>
);
}
Widget _buildSavedViewsTab(
ConnectivityState connectivityState,
BuildContext context,
) {
return RefreshIndicator(
edgeOffset: kTextTabBarHeight,
onRefresh: _onReloadSavedViews,
notificationPredicate: (_) => connectivityState.isConnected,
child: CustomScrollView(
key: const PageStorageKey<String>("savedViews"),
slivers: [
SliverOverlapInjector(
handle: searchBarHandle,
Widget _buildSavedViewChangedIndicator() {
return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final savedViewCubit = context.watch<SavedViewCubit>();
final activeView = savedViewCubit.state.maybeMap(
loaded: (savedViewState) {
if (state.filter.selectedView != null) {
return savedViewState.savedViews[state.filter.selectedView!];
}
return null;
},
orElse: () => null,
);
final viewHasChanged =
activeView != null && activeView.toDocumentFilter() != state.filter;
return AnimatedScale(
scale: viewHasChanged ? 1 : 0,
alignment: Alignment.bottomCenter,
duration: const Duration(milliseconds: 300),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: const EdgeInsets.only(bottom: 24),
child: Material(
borderRadius: BorderRadius.circular(24),
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.9),
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
onTap: () async {
await _updateCurrentSavedView();
setState(() {});
},
child: Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Text(
"Update selected view",
style: Theme.of(context).textTheme.labelLarge,
),
),
),
),
),
),
SliverOverlapInjector(
handle: savedViewsHandle,
),
const SavedViewList(),
],
),
);
},
);
}
// Widget _buildSavedViewsTab(
// ConnectivityState connectivityState,
// BuildContext context,
// ) {
// return RefreshIndicator(
// edgeOffset: kTextTabBarHeight,
// onRefresh: _onReloadSavedViews,
// notificationPredicate: (_) => connectivityState.isConnected,
// child: CustomScrollView(
// key: const PageStorageKey<String>("savedViews"),
// slivers: [
// SliverOverlapInjector(
// handle: searchBarHandle,
// ),
// SliverOverlapInjector(
// handle: savedViewsHandle,
// ),
// const SavedViewList(),
// ],
// ),
// );
// }
Widget _buildDocumentsTab(
ConnectivityState connectivityState,
BuildContext context,
@@ -393,6 +371,10 @@ class _DocumentsPageState extends State<DocumentsPage>
onNotification: (notification) {
// Listen for scroll notifications to load new data.
// Scroll controller does not work here due to nestedscrollview limitations.
final offset = notification.metrics.pixels;
if (offset > 128 && _savedViewsExpansionController.isExpanded) {
_savedViewsExpansionController.collapse();
}
final currState = context.read<DocumentsCubit>().state;
final max = notification.metrics.maxScrollExtent;
@@ -403,7 +385,6 @@ class _DocumentsPageState extends State<DocumentsPage>
return false;
}
final offset = notification.metrics.pixels;
if (offset >= max * 0.7) {
context
.read<DocumentsCubit>()
@@ -420,7 +401,7 @@ class _DocumentsPageState extends State<DocumentsPage>
return false;
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight,
edgeOffset: kTextTabBarHeight + 2,
onRefresh: _onReloadDocuments,
notificationPredicate: (_) => connectivityState.isConnected,
child: CustomScrollView(
@@ -434,6 +415,7 @@ class _DocumentsPageState extends State<DocumentsPage>
builder: (context, state) {
return SliverToBoxAdapter(
child: SavedViewsWidget(
controller: _savedViewsExpansionController,
onViewSelected: (view) {
final cubit = context.read<DocumentsCubit>();
if (state.filter.selectedView == view.id) {
@@ -449,6 +431,22 @@ class _DocumentsPageState extends State<DocumentsPage>
showSnackBar(context,
"Saved view successfully updated."); //TODO: INTL
},
onDeleteView: (view) async {
HapticFeedback.mediumImpact();
final shouldRemove = await showDialog(
context: context,
builder: (context) =>
ConfirmDeleteSavedViewDialog(view: view),
);
if (shouldRemove) {
final documentsCubit = context.read<DocumentsCubit>();
context.read<SavedViewCubit>().remove(view);
if (documentsCubit.state.filter.selectedView ==
view.id) {
documentsCubit.resetFilter();
}
}
},
filter: state.filter,
),
);
@@ -722,16 +720,10 @@ class _DocumentsPageState extends State<DocumentsPage>
Future<void> _onReloadDocuments() async {
try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<DocumentsCubit>().reload();
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
Future<void> _onReloadSavedViews() async {
try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<SavedViewCubit>().reload();
await Future.wait([
context.read<DocumentsCubit>().reload(),
context.read<SavedViewCubit>().reload(),
]);
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
@@ -754,22 +746,37 @@ class _DocumentsPageState extends State<DocumentsPage>
if (viewHasChanged) {
final discardChanges = await showDialog(
context: context,
builder: (context) => SavedViewChangedDialog(),
builder: (context) => const SavedViewChangedDialog(),
);
if (discardChanges == true) {
cubit.resetFilter();
// Reset
} else if (discardChanges == false) {
final newView = activeView.copyWith(
filterRules: FilterRule.fromFilter(cubit.state.filter),
);
final savedViewCubit2 = context.read<SavedViewCubit>();
await savedViewCubit2.update(newView);
showSnackBar(context, "Saved view successfully updated.");
_updateCurrentSavedView();
}
} else {
cubit.resetFilter();
}
}
Future<void> _updateCurrentSavedView() async {
final savedViewCubit = context.read<SavedViewCubit>();
final cubit = context.read<DocumentsCubit>();
final activeView = savedViewCubit.state.maybeMap(
loaded: (state) {
if (cubit.state.filter.selectedView != null) {
return state.savedViews[cubit.state.filter.selectedView!];
}
return null;
},
orElse: () => null,
);
if (activeView == null) return;
final newView = activeView.copyWith(
filterRules: FilterRule.fromFilter(cubit.state.filter),
);
await savedViewCubit.update(newView);
showSnackBar(context, "Saved view successfully updated.");
}
}

View File

@@ -31,92 +31,90 @@ class DocumentListItem extends DocumentItem {
@override
Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state;
return Material(
child: ListTile(
tileColor: backgroundColor,
dense: true,
selected: isSelected,
onTap: () => _onTap(),
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
onLongPress: () => onSelected?.call(document),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
children: [
AbsorbPointer(
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
return ListTile(
tileColor: backgroundColor,
dense: true,
selected: isSelected,
onTap: () => _onTap(),
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
onLongPress: onSelected != null ? () => onSelected!(document) : null,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
children: [
AbsorbPointer(
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
],
),
Text(
document.title,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
AbsorbPointer(
absorbing: isSelectionActive,
child: TagsWidget(
isClickable: isLabelClickable,
tags: document.tags
.where((e) => labels.tags.containsKey(e))
.map((e) => labels.tags[e]!)
.toList(),
onTagSelected: (id) => onTagSelected?.call(id),
),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RichText(
maxLines: 1,
],
),
Text(
document.title,
overflow: TextOverflow.ellipsis,
text: TextSpan(
text: DateFormat.yMMMd().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,
maxLines: 1,
),
AbsorbPointer(
absorbing: isSelectionActive,
child: TagsWidget(
isClickable: isLabelClickable,
tags: document.tags
.where((e) => labels.tags.containsKey(e))
.map((e) => labels.tags[e]!)
.toList(),
onTagSelected: (id) => onTagSelected?.call(id),
),
),
),
isThreeLine: document.tags.isNotEmpty,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: GestureDetector(
child: DocumentPreview(
document: document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: enableHeroAnimation,
),
),
),
contentPadding: const EdgeInsets.all(8.0),
],
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
text: DateFormat.yMMMd().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,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: GestureDetector(
child: DocumentPreview(
document: document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: enableHeroAnimation,
),
),
),
contentPadding: const EdgeInsets.all(8.0),
);
}

View File

@@ -16,12 +16,12 @@ class SavedViewChangedDialog extends StatelessWidget {
actionsOverflowButtonSpacing: 8,
actions: [
const DialogCancelButton(),
TextButton(
child: Text(S.of(context)!.saveChanges),
onPressed: () {
Navigator.pop(context, false);
},
),
// TextButton(
// child: Text(S.of(context)!.saveChanges),
// onPressed: () {
// Navigator.pop(context, false);
// },
// ),
DialogConfirmButton(
label: S.of(context)!.resetFilter,
style: DialogConfirmButtonStyle.danger,

View File

@@ -1,11 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_api/paperless_api.dart';
import 'dart:math';
class SavedViewChip extends StatelessWidget {
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
class SavedViewChip extends StatefulWidget {
final SavedView view;
final void Function(SavedView view) onViewSelected;
final void Function(SavedView vie) onUpdateView;
final void Function(SavedView view) onUpdateView;
final void Function(SavedView view) onDeleteView;
final bool selected;
final bool hasChanged;
@@ -16,28 +21,146 @@ class SavedViewChip extends StatelessWidget {
required this.selected,
required this.hasChanged,
required this.onUpdateView,
required this.onDeleteView,
});
@override
State<SavedViewChip> createState() => _SavedViewChipState();
}
class _SavedViewChipState extends State<SavedViewChip>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_animation = _animationController.drive(Tween(begin: 0, end: 1));
}
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Badge(
smallSize: 12,
alignment: const AlignmentDirectional(1.1, -1.2),
backgroundColor: Colors.red,
isLabelVisible: hasChanged,
child: FilterChip(
avatar: Icon(
Icons.saved_search,
color: Theme.of(context).colorScheme.onSurface,
var colorScheme = Theme.of(context).colorScheme;
final effectiveBackgroundColor = widget.selected
? colorScheme.secondaryContainer
: colorScheme.surfaceVariant;
final effectiveForegroundColor = widget.selected
? colorScheme.onSecondaryContainer
: colorScheme.onSurfaceVariant;
final expandedChild = Row(
children: [
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.edit,
color: effectiveForegroundColor,
),
onPressed: () {
EditSavedViewRoute(widget.view).push(context);
},
),
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.delete,
color: colorScheme.error,
),
onPressed: () async {
widget.onDeleteView(widget.view);
},
),
],
);
return Material(
color: effectiveBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: colorScheme.outline,
),
),
child: InkWell(
enableFeedback: true,
borderRadius: BorderRadius.circular(8),
onTap: () => widget.onViewSelected(widget.view),
child: Padding(
padding: const EdgeInsets.only(right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
_buildCheckmark(effectiveForegroundColor),
_buildLabel(context, effectiveForegroundColor)
.paddedSymmetrically(
horizontal: 12,
vertical: 0,
),
],
).paddedOnly(left: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: _isExpanded ? expandedChild : const SizedBox.shrink(),
),
_buildTrailing(effectiveForegroundColor),
],
),
),
showCheckmark: false,
selectedColor: Theme.of(context).colorScheme.primaryContainer,
selected: selected,
label: Text(view.name),
onSelected: (_) {
onViewSelected(view);
},
),
);
}
Widget _buildTrailing(Color effectiveForegroundColor) {
return IconButton(
icon: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.rotate(
angle: _animation.value * pi,
child: Icon(
_isExpanded ? Icons.close : Icons.chevron_right,
color: effectiveForegroundColor,
),
);
},
),
onPressed: () {
if (_isExpanded) {
_animationController.reverse();
} else {
_animationController.forward();
}
setState(() {
_isExpanded = !_isExpanded;
});
},
);
}
Widget _buildLabel(BuildContext context, Color effectiveForegroundColor) {
return Text(
widget.view.name,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: effectiveForegroundColor),
);
}
Widget _buildCheckmark(Color effectiveForegroundColor) {
return AnimatedSize(
duration: const Duration(milliseconds: 300),
child: widget.selected
? Icon(Icons.check, color: effectiveForegroundColor)
: const SizedBox.shrink(),
);
}
}

View File

@@ -1,57 +1,144 @@
import 'package:flutter/widgets.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/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_chip.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
class SavedViewsWidget extends StatelessWidget {
class SavedViewsWidget extends StatefulWidget {
final void Function(SavedView view) onViewSelected;
final void Function(SavedView view) onUpdateView;
final void Function(SavedView view) onDeleteView;
final DocumentFilter filter;
final ExpansionTileController? controller;
const SavedViewsWidget({
super.key,
required this.onViewSelected,
required this.filter,
required this.onUpdateView,
required this.onDeleteView,
this.controller,
});
@override
State<SavedViewsWidget> createState() => _SavedViewsWidgetState();
}
class _SavedViewsWidgetState extends State<SavedViewsWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_animation = _animationController.drive(Tween(begin: 0, end: 0.5));
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
height: 50,
child: BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
return state.maybeWhen(
loaded: (savedViews) {
if (savedViews.isEmpty) {
return Text("No saved views");
}
return ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final view = savedViews.values.elementAt(index);
return SavedViewChip(
view: view,
onUpdateView: onUpdateView,
onViewSelected: onViewSelected,
selected: filter.selectedView != null &&
view.id == filter.selectedView,
hasChanged: filter.selectedView == view.id &&
filter != view.toDocumentFilter(),
return PageStorage(
bucket: PageStorageBucket(),
child: ExpansionTile(
controller: widget.controller,
tilePadding: const EdgeInsets.only(left: 8),
trailing: RotationTransition(
turns: _animation,
child: const Icon(Icons.expand_more),
).paddedOnly(right: 8),
onExpansionChanged: (isExpanded) {
if (isExpanded) {
_animationController.forward();
} else {
_animationController.reverse().then((value) => setState(() {}));
}
},
title: Text(
S.of(context)!.views,
style: Theme.of(context).textTheme.labelLarge,
),
leading: Icon(
Icons.saved_search,
color: Theme.of(context).colorScheme.primary,
).padded(),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
return state.map(
initial: (_) => const Placeholder(),
loading: (_) => const Placeholder(),
loaded: (value) {
if (value.savedViews.isEmpty) {
return Text(S.of(context)!.noItemsFound)
.paddedOnly(left: 16);
}
return Container(
margin: EdgeInsets.only(top: 16),
height: kMinInteractiveDimension,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) => true,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: [
const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
SliverList.separated(
itemBuilder: (context, index) {
final view =
value.savedViews.values.elementAt(index);
final isSelected =
(widget.filter.selectedView ?? -1) == view.id;
return SavedViewChip(
view: view,
onViewSelected: widget.onViewSelected,
selected: isSelected,
hasChanged: isSelected &&
view.toDocumentFilter() != widget.filter,
onUpdateView: widget.onUpdateView,
onDeleteView: widget.onDeleteView,
);
},
separatorBuilder: (context, index) =>
const SizedBox(width: 8),
itemCount: value.savedViews.length,
),
const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
],
),
),
);
},
itemCount: savedViews.length,
error: (_) => const Placeholder(),
);
},
error: () => Text("Error loading saved views"),
orElse: () => Placeholder(),
);
},
),
Align(
alignment: Alignment.centerRight,
child: Tooltip(
message: "Create from current filter", //TODO: INTL
child: TextButton.icon(
onPressed: () {
CreateSavedViewRoute(widget.filter).push(context);
},
icon: const Icon(Icons.add),
label: Text(S.of(context)!.newView),
),
).padded(4),
),
],
),
);
}

View File

@@ -16,7 +16,7 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget {
Widget build(BuildContext context) {
return AlertDialog(
title: Text(
S.of(context)!.deleteView + view.name + "?",
S.of(context)!.deleteView(view.name),
softWrap: true,
),
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),

View File

@@ -37,9 +37,16 @@ class AddTagPage extends StatelessWidget {
.withOpacity(1.0),
readOnly: true,
),
FormBuilderCheckbox(
FormBuilderField<bool>(
name: Tag.isInboxTagKey,
title: Text(S.of(context)!.inboxTag),
initialValue: false,
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.inboxTag),
onChanged: (value) => field.didChange(value),
);
},
),
],
),

View File

@@ -38,10 +38,16 @@ class EditTagPage extends StatelessWidget {
colorPickerType: ColorPickerType.materialPicker,
readOnly: true,
),
FormBuilderCheckbox(
initialValue: tag.isInboxTag,
FormBuilderField<bool>(
name: Tag.isInboxTagKey,
title: Text(S.of(context)!.inboxTag),
initialValue: tag.isInboxTag,
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.inboxTag),
onChanged: (value) => field.didChange(value),
);
},
),
],
),

View File

@@ -137,10 +137,16 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
initialValue: widget.initialValue?.match,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderCheckbox(
FormBuilderField<bool>(
name: Label.isInsensitiveKey,
initialValue: widget.initialValue?.isInsensitive ?? true,
title: Text(S.of(context)!.caseIrrelevant),
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.caseIrrelevant),
onChanged: (value) => field.didChange(value),
);
},
),
...widget.additionalFields,
].padded(),

View File

@@ -55,9 +55,17 @@ class _LandingPageState extends State<LandingPage> {
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 16, 0, 8),
sliver: SliverToBoxAdapter(
child: Text(
"Saved Views",
style: Theme.of(context).textTheme.titleMedium,
child: Row(
children: [
Icon(
Icons.saved_search,
color: Theme.of(context).colorScheme.primary,
).paddedOnly(right: 8),
Text(
S.of(context)!.views,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
),
@@ -75,7 +83,7 @@ class _LandingPageState extends State<LandingPage> {
children: [
Text(
"There are no saved views to show on your dashboard.", //TODO: INTL
),
).padded(),
TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
@@ -89,6 +97,7 @@ class _LandingPageState extends State<LandingPage> {
itemBuilder: (context, index) {
return SavedViewPreview(
savedView: dashboardViews.elementAt(index),
expanded: index == 0,
);
},
itemCount: dashboardViews.length,

View File

@@ -29,7 +29,7 @@ class ExpansionCard extends StatelessWidget {
),
),
child: ExpansionTile(
backgroundColor: Theme.of(context).colorScheme.surface,
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
initiallyExpanded: initiallyExpanded,
title: title,
children: [content],

View File

@@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
const _fkName = 'name';
const _fkShowOnDashboard = 'show_on_dashboard';
const _fkShowInSidebar = 'show_in_sidebar';
class AddSavedViewPage extends StatefulWidget {
final DocumentFilter? initialFilter;
const AddSavedViewPage({
@@ -17,12 +23,7 @@ class AddSavedViewPage extends StatefulWidget {
}
class _AddSavedViewPageState extends State<AddSavedViewPage> {
static const fkName = 'name';
static const fkShowOnDashboard = 'show_on_dashboard';
static const fkShowInSidebar = 'show_in_sidebar';
final _savedViewFormKey = GlobalKey<FormBuilderState>();
final _filterFormKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -46,7 +47,7 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
child: Column(
children: [
FormBuilderTextField(
name: _AddSavedViewPageState.fkName,
name: _fkName,
validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
@@ -57,41 +58,53 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
label: Text(S.of(context)!.name),
),
),
FormBuilderCheckbox(
name: _AddSavedViewPageState.fkShowOnDashboard,
FormBuilderField<bool>(
name: _fkShowOnDashboard,
initialValue: false,
title: Text(S.of(context)!.showOnDashboard),
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.showOnDashboard),
onChanged: (value) => field.didChange(value),
);
},
),
FormBuilderCheckbox(
name: _AddSavedViewPageState.fkShowInSidebar,
FormBuilderField<bool>(
name: _fkShowInSidebar,
initialValue: false,
title: Text(S.of(context)!.showInSidebar),
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.showInSidebar),
onChanged: (value) => field.didChange(value),
);
},
),
],
),
),
const Divider(),
],
),
),
);
}
void _onCreate(BuildContext context) {
void _onCreate(BuildContext context) async {
if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) {
// context.pop(
// SavedView.fromDocumentFilter(
// DocumentFilterForm.assembleFilter(
// _filterFormKey,
// widget.currentFilter,
// ),
// name: _savedViewFormKey.currentState?.value[fkName] as String,
// showOnDashboard:
// _savedViewFormKey.currentState?.value[fkShowOnDashboard] as bool,
// showInSidebar:
// _savedViewFormKey.currentState?.value[fkShowInSidebar] as bool,
// ),
// );
final cubit = context.read<SavedViewCubit>();
var savedView = SavedView.fromDocumentFilter(
widget.initialFilter ?? const DocumentFilter(),
name: _savedViewFormKey.currentState?.value[_fkName] as String,
showOnDashboard:
_savedViewFormKey.currentState?.value[_fkShowOnDashboard] as bool,
showInSidebar:
_savedViewFormKey.currentState?.value[_fkShowInSidebar] as bool,
);
final router = GoRouter.of(context);
await cubit.add(
savedView,
);
router.pop();
}
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
const _fkName = 'name';
const _fkShowOnDashboard = 'show_on_dashboard';
const _fkShowInSidebar = 'show_in_sidebar';
class EditSavedViewPage extends StatefulWidget {
final SavedView savedView;
const EditSavedViewPage({
super.key,
required this.savedView,
});
@override
State<EditSavedViewPage> createState() => _EditSavedViewPageState();
}
class _EditSavedViewPageState extends State<EditSavedViewPage> {
final _savedViewFormKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.editView),
),
floatingActionButton: FloatingActionButton.extended(
heroTag: "fab_edit_saved_view_page",
icon: const Icon(Icons.save),
onPressed: () => _onCreate(context),
label: Text(S.of(context)!.saveChanges),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
FormBuilder(
key: _savedViewFormKey,
child: Column(
children: [
FormBuilderTextField(
initialValue: widget.savedView.name,
name: _fkName,
validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
}
return null;
},
decoration: InputDecoration(
label: Text(S.of(context)!.name),
),
),
FormBuilderField<bool>(
name: _fkShowOnDashboard,
initialValue: widget.savedView.showOnDashboard,
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.showOnDashboard),
onChanged: (value) => field.didChange(value),
);
},
),
FormBuilderField<bool>(
name: _fkShowInSidebar,
initialValue: widget.savedView.showInSidebar,
builder: (field) {
return CheckboxListTile(
value: field.value,
title: Text(S.of(context)!.showInSidebar),
onChanged: (value) => field.didChange(value),
);
},
),
],
),
),
],
),
),
);
}
void _onCreate(BuildContext context) async {
if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) {
final cubit = context.read<SavedViewCubit>();
var savedView = widget.savedView.copyWith(
name: _savedViewFormKey.currentState!.value[_fkName],
showInSidebar: _savedViewFormKey.currentState!.value[_fkShowInSidebar],
showOnDashboard:
_savedViewFormKey.currentState!.value[_fkShowOnDashboard],
);
final router = GoRouter.of(context);
await cubit.update(savedView);
router.pop();
}
}
}

View File

@@ -12,9 +12,11 @@ import 'package:provider/provider.dart';
class SavedViewPreview extends StatelessWidget {
final SavedView savedView;
final bool expanded;
const SavedViewPreview({
super.key,
required this.savedView,
required this.expanded,
});
@override
@@ -24,7 +26,7 @@ class SavedViewPreview extends StatelessWidget {
SavedViewPreviewCubit(context.read(), savedView)..initialize(),
builder: (context, child) {
return ExpansionCard(
initiallyExpanded: true,
initiallyExpanded: expanded,
title: Text(savedView.name),
content: BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
builder: (context, state) {
@@ -33,7 +35,7 @@ class SavedViewPreview extends StatelessWidget {
return Column(
children: [
if (documents.isEmpty)
Text("This view is empty.").padded()
Text("This view does not match any documents.").padded()
else
for (final document in documents)
DocumentListItem(
@@ -45,6 +47,7 @@ class SavedViewPreview extends StatelessWidget {
DocumentDetailsRoute($extra: document)
.push(context);
},
onSelected: null,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
@@ -64,7 +67,8 @@ class SavedViewPreview extends StatelessWidget {
],
);
},
error: () => const Text("Error loading preview"), //TODO: INTL
error: () =>
const Text("Could not load saved view."), //TODO: INTL
orElse: () => const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),

View File

@@ -15,9 +15,10 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
static const _languageOptions = {
'en': LanguageOption('English', true),
'de': LanguageOption('Deutsch', true),
'es': LanguageOption("Español", true),
'fr': LanguageOption('Français', true),
'cs': LanguageOption('Česky', true),
'tr': LanguageOption('Türkçe', true),
'fr': LanguageOption('Français', true),
'pl': LanguageOption('Polska', true),
'ca': LanguageOption('Catalan', true),
};

View File

@@ -25,6 +25,7 @@ void showSnackBar(
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: (details != null)
? RichText(
maxLines: 5,

View File

@@ -861,8 +861,20 @@
"@loginRequiredPermissionsHint": {
"description": "Hint shown on the login page informing the user of the required permissions to use the app."
},
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"missingPermissions": "No tens els permisos necessaris per a completar l'acció.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donate",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -864,5 +864,17 @@
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donate",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -864,5 +864,17 @@
"missingPermissions": "Sie besitzen nicht die benötigten Berechtigungen, um diese Aktion durchzuführen.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Ansicht bearbeiten",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Spenden",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Vielen Dank, dass Du diese App unterstützen möchtest! Aufgrund der Zahlungsrichtlinien von Google und Apple dürfen keine Links, die zu Spendenseiten führen, in der App angezeigt werden. Nicht einmal die Verlinkung zur Repository-Seite des Projekts scheint in diesem Zusammenhang erlaubt zu sein. Werfe von daher vielleicht einen Blick auf den Abschnitt 'Donations' in der README des Projekts. Deine Unterstützung ist sehr willkommen und hält die Entwicklung dieser App am Leben. Vielen Dank!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -864,5 +864,17 @@
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donate",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

880
lib/l10n/intl_es.arb Normal file
View File

@@ -0,0 +1,880 @@
{
"developedBy": "Desarrollado por {name}.",
"@developedBy": {
"placeholders": {
"name": {}
}
},
"addAnotherAccount": "Añadir otra cuenta",
"@addAnotherAccount": {},
"account": "Cuenta",
"@account": {},
"addCorrespondent": "Nuevo interlocutor",
"@addCorrespondent": {
"description": "Title when adding a new correspondent"
},
"addDocumentType": "Nuevo tipo de documento",
"@addDocumentType": {
"description": "Title when adding a new document type"
},
"addStoragePath": "Nueva ruta de almacenamiento",
"@addStoragePath": {
"description": "Title when adding a new storage path"
},
"addTag": "Nueva Etiqueta",
"@addTag": {
"description": "Title when adding a new tag"
},
"aboutThisApp": "Sobre esta app",
"@aboutThisApp": {
"description": "Label for about this app tile displayed in the drawer"
},
"loggedInAs": "Conectado como {name}",
"@loggedInAs": {
"placeholders": {
"name": {}
}
},
"disconnect": "Desconectar",
"@disconnect": {
"description": "Logout button label"
},
"reportABug": "Reportar un problema",
"@reportABug": {},
"settings": "Ajustes",
"@settings": {},
"authenticateOnAppStart": "Autenticar al iniciar la aplicación",
"@authenticateOnAppStart": {
"description": "Description of the biometric authentication settings tile"
},
"biometricAuthentication": "Autenticación biométrica",
"@biometricAuthentication": {},
"authenticateToToggleBiometricAuthentication": "{mode, select, enable{Autenticar para habilitar la autenticación biométrica} disable{Autenticar para deshabilitar la autenticación biométrica} other{}}",
"@authenticateToToggleBiometricAuthentication": {
"placeholders": {
"mode": {}
}
},
"documents": "Documentos",
"@documents": {},
"inbox": "Buzón",
"@inbox": {},
"labels": "Etiquetas",
"@labels": {},
"scanner": "Escáner",
"@scanner": {},
"startTyping": "Empezar a escribir...",
"@startTyping": {},
"doYouReallyWantToDeleteThisView": "¿Realmente desea eliminar esta vista?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "¿Eliminar vista {name}?",
"@deleteView": {},
"addedAt": "Añadido En",
"@addedAt": {},
"archiveSerialNumber": "Número de serie del archivo",
"@archiveSerialNumber": {},
"asn": "NSA",
"@asn": {},
"correspondent": "Interlocutor",
"@correspondent": {},
"createdAt": "Creado en",
"@createdAt": {},
"documentSuccessfullyDeleted": "Documento eliminado correctamente.",
"@documentSuccessfullyDeleted": {},
"assignAsn": "Asignar NSA",
"@assignAsn": {},
"deleteDocumentTooltip": "Eliminar",
"@deleteDocumentTooltip": {
"description": "Tooltip shown for the delete button on details page"
},
"downloadDocumentTooltip": "Descargar",
"@downloadDocumentTooltip": {
"description": "Tooltip shown for the download button on details page"
},
"editDocumentTooltip": "Editar",
"@editDocumentTooltip": {
"description": "Tooltip shown for the edit button on details page"
},
"loadFullContent": "Cargar el contenido completo",
"@loadFullContent": {},
"noAppToDisplayPDFFilesFound": "¡No se encontraron aplicaciones para mostrar archivos PDF!",
"@noAppToDisplayPDFFilesFound": {},
"openInSystemViewer": "Abrir en el visor del sistema",
"@openInSystemViewer": {},
"couldNotOpenFilePermissionDenied": "No se pudo abrir el archivo: Permiso denegado.",
"@couldNotOpenFilePermissionDenied": {},
"previewTooltip": "Vista previa",
"@previewTooltip": {
"description": "Tooltip shown for the preview button on details page"
},
"shareTooltip": "Compartir",
"@shareTooltip": {
"description": "Tooltip shown for the share button on details page"
},
"similarDocuments": "Documentos similares",
"@similarDocuments": {
"description": "Label shown in the tabbar on details page"
},
"content": "Contenido",
"@content": {
"description": "Label shown in the tabbar on details page"
},
"metaData": "Metadatos",
"@metaData": {
"description": "Label shown in the tabbar on details page"
},
"overview": "Vista general",
"@overview": {
"description": "Label shown in the tabbar on details page"
},
"documentType": "Tipo de Documento",
"@documentType": {},
"archivedPdf": "Archivado (pdf)",
"@archivedPdf": {
"description": "Option to chose when downloading a document"
},
"chooseFiletype": "Elegir tipo de archivo",
"@chooseFiletype": {},
"original": "Original",
"@original": {
"description": "Option to chose when downloading a document"
},
"documentSuccessfullyDownloaded": "Documento descargado correctamente.",
"@documentSuccessfullyDownloaded": {},
"suggestions": "Sugerencias: ",
"@suggestions": {},
"editDocument": "Editar Documento",
"@editDocument": {},
"advanced": "Avanzado",
"@advanced": {},
"apply": "Aplicar",
"@apply": {},
"extended": "Extendido",
"@extended": {},
"titleAndContent": "Título y Contenido",
"@titleAndContent": {},
"title": "Título",
"@title": {},
"reset": "Restablecer",
"@reset": {},
"filterDocuments": "Filtrar Documentos",
"@filterDocuments": {
"description": "Title of the document filter"
},
"originalMD5Checksum": "Verificación MD5",
"@originalMD5Checksum": {},
"mediaFilename": "Nombre del archivo",
"@mediaFilename": {},
"originalFileSize": "Tamaño del archivo original",
"@originalFileSize": {},
"originalMIMEType": "Tipo MIME Original",
"@originalMIMEType": {},
"modifiedAt": "Modificado en",
"@modifiedAt": {},
"preview": "Vista previa",
"@preview": {
"description": "Title of the document preview page"
},
"scanADocument": "Escanear documento",
"@scanADocument": {},
"noDocumentsScannedYet": "No hay documentos escaneados.",
"@noDocumentsScannedYet": {},
"or": "o",
"@or": {
"description": "Used on the scanner page between both main actions when no scans have been captured."
},
"deleteAllScans": "Eliminar todos los escaneos",
"@deleteAllScans": {},
"uploadADocumentFromThisDevice": "Subir un documento desde este dispositivo",
"@uploadADocumentFromThisDevice": {
"description": "Button label on scanner page"
},
"noMatchesFound": "No se encontraron documentos.",
"@noMatchesFound": {
"description": "Displayed when no documents were found in the document search."
},
"removeFromSearchHistory": "¿Eliminar del historial de búsqueda?",
"@removeFromSearchHistory": {},
"results": "Resultados",
"@results": {
"description": "Label displayed above search results in document search."
},
"searchDocuments": "Buscar documentos",
"@searchDocuments": {},
"resetFilter": "Limpiar filtro",
"@resetFilter": {},
"lastMonth": "Último Mes",
"@lastMonth": {},
"last7Days": "Últimos 7 días",
"@last7Days": {},
"last3Months": "Últimos 3 meses",
"@last3Months": {},
"lastYear": "Último año",
"@lastYear": {},
"search": "Buscar",
"@search": {},
"documentsSuccessfullyDeleted": "Documentos eliminados correctamente.",
"@documentsSuccessfullyDeleted": {},
"thereSeemsToBeNothingHere": "Parece que no hay nada aquí...",
"@thereSeemsToBeNothingHere": {},
"oops": "Ups.",
"@oops": {},
"newDocumentAvailable": "¡Nuevo documento disponible!",
"@newDocumentAvailable": {},
"orderBy": "Ordenar por",
"@orderBy": {},
"thisActionIsIrreversibleDoYouWishToProceedAnyway": "Esta acción es irreversible. ¿Desea continuar?",
"@thisActionIsIrreversibleDoYouWishToProceedAnyway": {},
"confirmDeletion": "Confirmar eliminación",
"@confirmDeletion": {},
"areYouSureYouWantToDeleteTheFollowingDocuments": "{count, plural, one{¿Está seguro de querer eliminar el siguiente documento?} other{¿Está seguro de querer eliminar los siguientes documentos?}}",
"@areYouSureYouWantToDeleteTheFollowingDocuments": {
"placeholders": {
"count": {}
}
},
"countSelected": "{count} en selección",
"@countSelected": {
"description": "Displayed in the appbar when at least one document is selected.",
"placeholders": {
"count": {}
}
},
"storagePath": "Ruta de Almacenamiento",
"@storagePath": {},
"prepareDocument": "Preparar documento",
"@prepareDocument": {},
"tags": "Etiquetas",
"@tags": {},
"documentSuccessfullyUpdated": "Documento actualizado correctamente.",
"@documentSuccessfullyUpdated": {},
"fileName": "Nombre del archivo",
"@fileName": {},
"synchronizeTitleAndFilename": "Sincronizar título y nombre del archivo",
"@synchronizeTitleAndFilename": {},
"reload": "Actualizar",
"@reload": {},
"documentSuccessfullyUploadedProcessing": "Documento subido correctamente, procesando...",
"@documentSuccessfullyUploadedProcessing": {},
"deleteLabelWarningText": "Esta etiqueta contiene referencias a otros documentos. Al eliminar esta etiqueta, todas las referencias serán eliminadas. ¿Desea continuar?",
"@deleteLabelWarningText": {},
"couldNotAcknowledgeTasks": "No se han podido reconocer las tareas.",
"@couldNotAcknowledgeTasks": {},
"authenticationFailedPleaseTryAgain": "Error de autenticación, intente nuevamente.",
"@authenticationFailedPleaseTryAgain": {},
"anErrorOccurredWhileTryingToAutocompleteYourQuery": "Ha ocurrido un error intentando completar su búsqueda.",
"@anErrorOccurredWhileTryingToAutocompleteYourQuery": {},
"biometricAuthenticationFailed": "Falló la autenticación biométrica.",
"@biometricAuthenticationFailed": {},
"biometricAuthenticationNotSupported": "La autenticación biométrica no es compatible con este dispositivo.",
"@biometricAuthenticationNotSupported": {},
"couldNotBulkEditDocuments": "No se han podido editar masivamente los documentos.",
"@couldNotBulkEditDocuments": {},
"couldNotCreateCorrespondent": "No se ha podido crear el interlocutor, intente nuevamente.",
"@couldNotCreateCorrespondent": {},
"couldNotLoadCorrespondents": "No se han podido cargar interlocutores.",
"@couldNotLoadCorrespondents": {},
"couldNotCreateSavedView": "No se ha podido guardar la vista, intente nuevamente.",
"@couldNotCreateSavedView": {},
"couldNotDeleteSavedView": "No se ha podido eliminar la vista, intente nuevamente",
"@couldNotDeleteSavedView": {},
"youAreCurrentlyOffline": "Estás desconectado. Asegúrate de estar conectado a internet.",
"@youAreCurrentlyOffline": {},
"couldNotAssignArchiveSerialNumber": "No se pudo asignar número de serie al archivo.",
"@couldNotAssignArchiveSerialNumber": {},
"couldNotDeleteDocument": "No se ha podido eliminar el documento, intente nuevamente.",
"@couldNotDeleteDocument": {},
"couldNotLoadDocuments": "No se han podido cargar los documentos, intente nuevamente.",
"@couldNotLoadDocuments": {},
"couldNotLoadDocumentPreview": "No se ha podido cargar la vista previa del documento.",
"@couldNotLoadDocumentPreview": {},
"couldNotCreateDocument": "No se ha podido crear el documento, intente nuevamente.",
"@couldNotCreateDocument": {},
"couldNotLoadDocumentTypes": "No se han podido cargar los tipos de documento, intente nuevamente.",
"@couldNotLoadDocumentTypes": {},
"couldNotUpdateDocument": "No se ha podido actualizar el documento, intente nuevamente.",
"@couldNotUpdateDocument": {},
"couldNotUploadDocument": "No se ha podido subir el documento, intente nuevamente.",
"@couldNotUploadDocument": {},
"invalidCertificateOrMissingPassphrase": "Certificado inválido o falta la frase de seguridad, intente nuevamente",
"@invalidCertificateOrMissingPassphrase": {},
"couldNotLoadSavedViews": "No se han podido cargar las vistas guardadas.",
"@couldNotLoadSavedViews": {},
"aClientCertificateWasExpectedButNotSent": "Se esperaba un certificado de cliente pero no se ha enviado. Proporcione un certificado de cliente válido.",
"@aClientCertificateWasExpectedButNotSent": {},
"userIsNotAuthenticated": "Usuario no autenticado.",
"@userIsNotAuthenticated": {},
"requestTimedOut": "La petición al servidor ha superado el tiempo de espera.",
"@requestTimedOut": {},
"anErrorOccurredRemovingTheScans": "Ha ocurrido un error eliminando los escaneos.",
"@anErrorOccurredRemovingTheScans": {},
"couldNotReachYourPaperlessServer": "No se ha podido conectar con el servidor de Paperless, ¿Está funcionando?",
"@couldNotReachYourPaperlessServer": {},
"couldNotLoadSimilarDocuments": "No se han podido cargar documentos similares.",
"@couldNotLoadSimilarDocuments": {},
"couldNotCreateStoragePath": "No se ha podido crear la ruta de almacenamiento, intente nuevamente.",
"@couldNotCreateStoragePath": {},
"couldNotLoadStoragePaths": "No se han podido cargar las rutas de almacenamiento.",
"@couldNotLoadStoragePaths": {},
"couldNotLoadSuggestions": "No se han podido cargar sugerencias.",
"@couldNotLoadSuggestions": {},
"couldNotCreateTag": "No se ha podido crear la etiqueta, intente nuevamente.",
"@couldNotCreateTag": {},
"couldNotLoadTags": "No se han podido cargar las etiquetas.",
"@couldNotLoadTags": {},
"anUnknownErrorOccurred": "Ocurrió un error desconocido.",
"@anUnknownErrorOccurred": {},
"fileFormatNotSupported": "Formato de archivo no compatible.",
"@fileFormatNotSupported": {},
"report": "INFORMAR",
"@report": {},
"absolute": "Absoluto",
"@absolute": {},
"hintYouCanAlsoSpecifyRelativeValues": "Consejo: Además de fechas concretas, puedes especificar un intervalo de tiempo relativo a la fecha actual.",
"@hintYouCanAlsoSpecifyRelativeValues": {
"description": "Displayed in the extended date range picker"
},
"amount": "Cantidad",
"@amount": {},
"relative": "Relativo",
"@relative": {},
"last": "Último",
"@last": {},
"timeUnit": "Unidad de tiempo",
"@timeUnit": {},
"selectDateRange": "Seleccione el intervalo de fechas",
"@selectDateRange": {},
"after": "Después",
"@after": {},
"before": "Antes",
"@before": {},
"days": "{count, plural, one{día} other{días}}",
"@days": {
"placeholders": {
"count": {}
}
},
"lastNDays": "{count, plural, one{Ayer} other{Últimos {count} días}}",
"@lastNDays": {
"placeholders": {
"count": {}
}
},
"lastNMonths": "{count, plural, one{Último mes} other{Últimos {count} meses}}",
"@lastNMonths": {
"placeholders": {
"count": {}
}
},
"lastNWeeks": "{count, plural, one{Última semana} other{Últimas {count} semanas}}",
"@lastNWeeks": {
"placeholders": {
"count": {}
}
},
"lastNYears": "{count, plural, one{Último año} other{Últimos {count} años}}",
"@lastNYears": {
"placeholders": {
"count": {}
}
},
"months": "{count, plural, one{mes} other{meses}}",
"@months": {
"placeholders": {
"count": {}
}
},
"weeks": "{count, plural, one{semana} other{semanas}}",
"@weeks": {
"placeholders": {
"count": {}
}
},
"years": "{count, plural, one{año} other{años}}",
"@years": {
"placeholders": {
"count": {}
}
},
"gotIt": "¡Entendido!",
"@gotIt": {},
"cancel": "Cancelar",
"@cancel": {},
"close": "Cerrar",
"@close": {},
"create": "Crear",
"@create": {},
"delete": "Eliminar",
"@delete": {},
"edit": "Editar",
"@edit": {},
"ok": "Aceptar",
"@ok": {},
"save": "Guardar",
"@save": {},
"select": "Seleccionar",
"@select": {},
"saveChanges": "Guardar cambios",
"@saveChanges": {},
"upload": "Subir",
"@upload": {},
"youreOffline": "Estás desconectado.",
"@youreOffline": {},
"deleteDocument": "Eliminar documento",
"@deleteDocument": {
"description": "Used as an action label on each inbox item"
},
"removeDocumentFromInbox": "Documento eliminado del buzón.",
"@removeDocumentFromInbox": {},
"areYouSureYouWantToMarkAllDocumentsAsSeen": "¿Está seguro de marcar todos los documentos como leídos? Esto realizará una edición masiva que eliminará todas las etiquetas de entrada de los documentos. ¡Esta acción no es reversible! ¿Desea continuar?",
"@areYouSureYouWantToMarkAllDocumentsAsSeen": {},
"markAllAsSeen": "¿Marcar todos como leídos?",
"@markAllAsSeen": {},
"allSeen": "Todos leídos",
"@allSeen": {},
"markAsSeen": "Marcar como leído",
"@markAsSeen": {},
"refresh": "Recargar",
"@refresh": {},
"youDoNotHaveUnseenDocuments": "No tienes documentos no leídos.",
"@youDoNotHaveUnseenDocuments": {},
"quickAction": "Acción rápida",
"@quickAction": {},
"suggestionSuccessfullyApplied": "Sugerencia aplicada correctamente.",
"@suggestionSuccessfullyApplied": {},
"today": "Hoy",
"@today": {},
"undo": "Deshacer",
"@undo": {},
"nUnseen": "{count} no leídos",
"@nUnseen": {
"placeholders": {
"count": {}
}
},
"swipeLeftToMarkADocumentAsSeen": "Consejo: Deslice a la izquierda para marcar un documento como leído y elimina todas la etiquetas de entrada del documento.",
"@swipeLeftToMarkADocumentAsSeen": {},
"yesterday": "Ayer",
"@yesterday": {},
"anyAssigned": "Cualquier asignado",
"@anyAssigned": {},
"noItemsFound": "¡No se han encontrado elementos!",
"@noItemsFound": {},
"caseIrrelevant": "Sin distinción mayúscula/minúscula",
"@caseIrrelevant": {},
"matchingAlgorithm": "Algoritmo de coincidencia",
"@matchingAlgorithm": {},
"match": "Coincidencia",
"@match": {},
"name": "Nombre",
"@name": {},
"notAssigned": "Sin asignar",
"@notAssigned": {},
"addNewCorrespondent": "Añadir nuevo interlocutor",
"@addNewCorrespondent": {},
"noCorrespondentsSetUp": "Parece que no tienes ningún interlocutor configurado.",
"@noCorrespondentsSetUp": {},
"correspondents": "Interlocutores",
"@correspondents": {},
"addNewDocumentType": "Añadir nuevo tipo de documento",
"@addNewDocumentType": {},
"noDocumentTypesSetUp": "Parece que no tienes ningún tipo de documento configurado.",
"@noDocumentTypesSetUp": {},
"documentTypes": "Tipos de Documentos",
"@documentTypes": {},
"addNewStoragePath": "Agregar nueva ruta de almacenamiento",
"@addNewStoragePath": {},
"noStoragePathsSetUp": "Parece que no tienes ninguna ruta de almacenamiento configurada.",
"@noStoragePathsSetUp": {},
"storagePaths": "Rutas de Almacenamiento",
"@storagePaths": {},
"addNewTag": "Agregar nueva etiqueta",
"@addNewTag": {},
"noTagsSetUp": "Parece que no tienes ninguna etiqueta configurada.",
"@noTagsSetUp": {},
"linkedDocuments": "Documentos vinculados",
"@linkedDocuments": {},
"advancedSettings": "Ajustes Avanzados",
"@advancedSettings": {},
"passphrase": "Frase de seguridad",
"@passphrase": {},
"configureMutualTLSAuthentication": "Configurar Autenticación Mutua TLS",
"@configureMutualTLSAuthentication": {},
"invalidCertificateFormat": "Formato de certificado inválido, solo se permite .pfx",
"@invalidCertificateFormat": {},
"clientcertificate": "Certificado de Cliente",
"@clientcertificate": {},
"selectFile": "Seleccionar archivo...",
"@selectFile": {},
"continueLabel": "Continuar",
"@continueLabel": {},
"incorrectOrMissingCertificatePassphrase": "Frase de seguridad del certificado no encontrada o incorrecta.",
"@incorrectOrMissingCertificatePassphrase": {},
"connect": "Conectar",
"@connect": {},
"password": "Contraseña",
"@password": {},
"passwordMustNotBeEmpty": "La contraseña no debe estar vacía.",
"@passwordMustNotBeEmpty": {},
"connectionTimedOut": "Tiempo de conexión agotado.",
"@connectionTimedOut": {},
"loginPageReachabilityMissingClientCertificateText": "Se esperaba un certificado de cliente pero no se ha enviado. Proporcione un certificado de cliente válido.",
"@loginPageReachabilityMissingClientCertificateText": {},
"couldNotEstablishConnectionToTheServer": "No se ha podido establecer una conexión con el servidor.",
"@couldNotEstablishConnectionToTheServer": {},
"connectionSuccessfulylEstablished": "La conexión se ha establecido correctamente.",
"@connectionSuccessfulylEstablished": {},
"hostCouldNotBeResolved": "El host no pudo ser resuelto. Por favor, compruebe la dirección del servidor y su conexión a Internet. ",
"@hostCouldNotBeResolved": {},
"serverAddress": "Dirección del servidor",
"@serverAddress": {},
"invalidAddress": "Dirección inválida.",
"@invalidAddress": {},
"serverAddressMustIncludeAScheme": "La dirección del servidor debe incluir un esquema.",
"@serverAddressMustIncludeAScheme": {},
"serverAddressMustNotBeEmpty": "La dirección del servidor no puede estar vacía.",
"@serverAddressMustNotBeEmpty": {},
"signIn": "Iniciar sesión",
"@signIn": {},
"loginPageSignInTitle": "Iniciar sesión",
"@loginPageSignInTitle": {},
"signInToServer": "Iniciar sesión en {serverAddress}",
"@signInToServer": {
"placeholders": {
"serverAddress": {}
}
},
"connectToPaperless": "Conectar a Paperless",
"@connectToPaperless": {},
"username": "Usuario",
"@username": {},
"usernameMustNotBeEmpty": "Usuario no puede estar vacío.",
"@usernameMustNotBeEmpty": {},
"documentContainsAllOfTheseWords": "El documento contiene todas estas palabras",
"@documentContainsAllOfTheseWords": {},
"all": "Todo",
"@all": {},
"documentContainsAnyOfTheseWords": "El documento contiene cualquiera de estas palabras",
"@documentContainsAnyOfTheseWords": {},
"any": "Cualquiera",
"@any": {},
"learnMatchingAutomatically": "Aprendizaje automático",
"@learnMatchingAutomatically": {},
"auto": "Auto",
"@auto": {},
"documentContainsThisString": "El documento contiene este texto",
"@documentContainsThisString": {},
"exact": "Exacto",
"@exact": {},
"documentContainsAWordSimilarToThisWord": "El documento contiene una palabra similar a esta",
"@documentContainsAWordSimilarToThisWord": {},
"fuzzy": "Similar",
"@fuzzy": {},
"documentMatchesThisRegularExpression": "El documento coincide con la expresión regular",
"@documentMatchesThisRegularExpression": {},
"regularExpression": "Expresión regular",
"@regularExpression": {},
"anInternetConnectionCouldNotBeEstablished": "No se ha podido establecer una conexión a internet.",
"@anInternetConnectionCouldNotBeEstablished": {},
"done": "Hecho",
"@done": {},
"next": "Siguiente",
"@next": {},
"couldNotAccessReceivedFile": "No se ha podido acceder al archivo recibido. Intente abrir la app antes de compartir.",
"@couldNotAccessReceivedFile": {},
"newView": "Nueva vista",
"@newView": {},
"createsASavedViewBasedOnTheCurrentFilterCriteria": "Crea una nueva vista basada en el actual criterio de filtrado.",
"@createsASavedViewBasedOnTheCurrentFilterCriteria": {},
"createViewsToQuicklyFilterYourDocuments": "Crea vistas para filtrar rápidamente tus documentos.",
"@createViewsToQuicklyFilterYourDocuments": {},
"nFiltersSet": "{count, plural, one{{count} filtro aplicado} other{{count} filtros aplicados}}",
"@nFiltersSet": {
"placeholders": {
"count": {}
}
},
"showInSidebar": "Mostrar en la barra lateral",
"@showInSidebar": {},
"showOnDashboard": "Mostrar en el panel",
"@showOnDashboard": {},
"views": "Vistas",
"@views": {},
"clearAll": "Limpiar todo",
"@clearAll": {},
"scan": "Escanear",
"@scan": {},
"previewScan": "Vista previa",
"@previewScan": {},
"scrollToTop": "Volver arriba",
"@scrollToTop": {},
"paperlessServerVersion": "Versión del servidor Paperless",
"@paperlessServerVersion": {},
"darkTheme": "Tema Oscuro",
"@darkTheme": {},
"lightTheme": "Tema Claro",
"@lightTheme": {},
"systemTheme": "Usar tema del sistema",
"@systemTheme": {},
"appearance": "Apariencia",
"@appearance": {},
"languageAndVisualAppearance": "Idioma y apariencia visual",
"@languageAndVisualAppearance": {},
"applicationSettings": "Aplicación",
"@applicationSettings": {},
"colorSchemeHint": "Elija entre un esquema de colores clásicos, inspirado en el verde tradicional de Paperless, o utilice un esquema de color dinámico, basado en el tema del sistema.",
"@colorSchemeHint": {},
"colorSchemeNotSupportedWarning": "El tema dinámico solamente es compatible con dispositivos con Android 12 o superior. Seleccionar la opción 'Dinámico' podría no tener efecto dependiendo de la implementación en su sistema operativo.",
"@colorSchemeNotSupportedWarning": {},
"colors": "Colores",
"@colors": {},
"language": "Idioma",
"@language": {},
"security": "Seguridad",
"@security": {},
"mangeFilesAndStorageSpace": "Administre los archivos y el espacio de almacenamiento",
"@mangeFilesAndStorageSpace": {},
"storage": "Almacenamiento",
"@storage": {},
"dark": "Oscuro",
"@dark": {},
"light": "Claro",
"@light": {},
"system": "Sistema",
"@system": {},
"ascending": "Ascendente",
"@ascending": {},
"descending": "Descendente",
"@descending": {},
"storagePathDay": "día",
"@storagePathDay": {},
"storagePathMonth": "mes",
"@storagePathMonth": {},
"storagePathYear": "año",
"@storagePathYear": {},
"color": "Color",
"@color": {},
"filterTags": "Filtrar etiquetas...",
"@filterTags": {},
"inboxTag": "Etiqueta de entrada",
"@inboxTag": {},
"uploadInferValuesHint": "Si especifica valores en estos campos, su instancia de Paperless no obtendrá un valor automáticamente. Deje estos campos en blanco si quiere que estos valores sean completados por el servidor.",
"@uploadInferValuesHint": {},
"useTheConfiguredBiometricFactorToAuthenticate": "Usar el factor biométrico configurado para autenticar y desbloquear sus documentos.",
"@useTheConfiguredBiometricFactorToAuthenticate": {},
"verifyYourIdentity": "Verifica tu identidad",
"@verifyYourIdentity": {},
"verifyIdentity": "Verificar identidad",
"@verifyIdentity": {},
"detailed": "Detallado",
"@detailed": {},
"grid": "Cuadrícula",
"@grid": {},
"list": "Lista",
"@list": {},
"remove": "Eliminar",
"removeQueryFromSearchHistory": "¿Eliminar consulta del historial de búsqueda?",
"dynamicColorScheme": "Dinámico",
"@dynamicColorScheme": {},
"classicColorScheme": "Clásico",
"@classicColorScheme": {},
"notificationDownloadComplete": "Descarga completada",
"@notificationDownloadComplete": {
"description": "Notification title when a download has been completed."
},
"notificationDownloadingDocument": "Descargando documento",
"@notificationDownloadingDocument": {
"description": "Notification title shown when a document download is pending"
},
"archiveSerialNumberUpdated": "Número de serie del archivo actualizado.",
"@archiveSerialNumberUpdated": {
"description": "Message shown when the ASN has been updated."
},
"donateCoffee": "Invítame a un café",
"@donateCoffee": {
"description": "Label displayed in the app drawer"
},
"thisFieldIsRequired": "¡Este campo es obligatorio!",
"@thisFieldIsRequired": {
"description": "Message shown below the form field when a required field has not been filled out."
},
"confirm": "Confirmar",
"confirmAction": "Confirmar acción",
"@confirmAction": {
"description": "Typically used as a title to confirm a previously selected action"
},
"areYouSureYouWantToContinue": "¿Seguro que quieres continuar?",
"bulkEditTagsAddMessage": "{count, plural, one{Esta acción agregará las etiquetas {tags} al documento seleccionado.} other{Esta acción agregará las etiquetas {tags} a los {count} documentos seleccionados.}}",
"@bulkEditTagsAddMessage": {
"description": "Message of the confirmation dialog when bulk adding tags."
},
"bulkEditTagsRemoveMessage": "{count, plural, one{Esta acción eliminará las etiquetas {tags} del documento seleccionado.} other{Esta acción eliminará las etiquetas {tags} de los {count} documentos seleccionados.}}",
"@bulkEditTagsRemoveMessage": {
"description": "Message of the confirmation dialog when bulk removing tags."
},
"bulkEditTagsModifyMessage": "{count, plural, one{Esta acción agregará las etiquetas {addTags} y eliminará las etiquetas {removeTags} del documento seleccionado.} other{Esta acción agregará las etiquetas {addTags} y eliminará las etiquetas {removeTags} de los {count} documentos seleccionados.}}",
"@bulkEditTagsModifyMessage": {
"description": "Message of the confirmation dialog when both adding and removing tags."
},
"bulkEditCorrespondentAssignMessage": "{count, plural, one{Esta acción asignará el interlocutor {correspondent} al documento seleccionado.} other{Esta operación asignará al interlocutor {correspondent} a los {count} documentos seleccionados.}}",
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{Esta acción asignará el tipo de documento {docType} al documento seleccionado.} other{Esta acción asignará el tipo de documento {docType} a los {count} documentos seleccionados.}}",
"bulkEditStoragePathAssignMessage": "{count, plural, one{Esta acción asignará la ruta de almacenamiento {path} al documento seleccionado.} other{Esta acción asignará la ruta de almacenamiento {path} a los {count} documentos seleccionados.}}",
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{Esta acción eliminará al interlocutor del documento seleccionado.} other{Esta acción eliminará al interlocutor de los {count} documentos seleccionados.}}",
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{Esta acción eliminará el tipo de documento del documento seleccionado.} other{Esta acción eliminará el tipo de documento de los {count} documentos seleccionados.}}",
"bulkEditStoragePathRemoveMessage": "{count, plural, one{Esta acción eliminará la ruta de almacenamiento del documento seleccionado.} other{Esta acción eliminará la ruta de almacenamiento de los {count} documentos seleccionados.}}",
"anyTag": "Cualquiera",
"@anyTag": {
"description": "Label shown when any tag should be filtered"
},
"allTags": "Todo",
"@allTags": {
"description": "Label shown when a document has to be assigned to all selected tags"
},
"switchingAccountsPleaseWait": "Cambiando cuentas. Por favor, espere...",
"@switchingAccountsPleaseWait": {
"description": "Message shown while switching accounts is in progress."
},
"testConnection": "Prueba de conexión",
"@testConnection": {
"description": "Button label shown on login page. Allows user to test whether the server is reachable or not."
},
"accounts": "Cuentas",
"@accounts": {
"description": "Title of the account management dialog"
},
"addAccount": "Añadir cuenta",
"@addAccount": {
"description": "Label of add account action"
},
"switchAccount": "Cambiar",
"@switchAccount": {
"description": "Label for switch account action"
},
"logout": "Cerrar sesión",
"@logout": {
"description": "Generic Logout label"
},
"switchAccountTitle": "Cambiar de cuenta",
"@switchAccountTitle": {
"description": "Title of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not."
},
"switchToNewAccount": "¿Quiere cambiar a una nueva cuenta? Puedes volver a la anterior en cualquier momento.",
"@switchToNewAccount": {
"description": "Content of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not."
},
"sourceCode": "Código Fuente",
"findTheSourceCodeOn": "Encuentra el código fuente en",
"@findTheSourceCodeOn": {
"description": "Text before link to Paperless Mobile GitHub"
},
"rememberDecision": "Recuerda mi decisión",
"defaultDownloadFileType": "Tipo de archivo predeterminado para descargar",
"@defaultDownloadFileType": {
"description": "Label indicating the default filetype to download (one of archived, original and always ask)"
},
"defaultShareFileType": "Tipo de archivo predeterminado para compartir",
"@defaultShareFileType": {
"description": "Label indicating the default filetype to share (one of archived, original and always ask)"
},
"alwaysAsk": "Preguntar siempre",
"@alwaysAsk": {
"description": "Option to choose when the app should always ask the user which filetype to use"
},
"disableMatching": "No etiquetar archivos automáticamente",
"@disableMatching": {
"description": "One of the options for automatic tagging of documents"
},
"none": "Ninguno",
"@none": {
"description": "One of available enum values of matching algorithm for tags"
},
"logInToExistingAccount": "Iniciar sesión en una cuenta existente",
"@logInToExistingAccount": {
"description": "Title shown on login page if at least one user is already known to the app."
},
"print": "Imprimir",
"@print": {
"description": "Tooltip for print button"
},
"managePermissions": "Administrar permisos",
"@managePermissions": {
"description": "Button which leads user to manage permissions page"
},
"errorRetrievingServerVersion": "Ocurrió un error intentando determinar la versión del servidor.",
"@errorRetrievingServerVersion": {
"description": "Message shown at the bottom of the settings page when the remote server version could not be resolved."
},
"resolvingServerVersion": "Determinando la versión del servidor...",
"@resolvingServerVersion": {
"description": "Message shown while the app is loading the remote server version."
},
"goToLogin": "Ir al inicio de sesión",
"@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
},
"export": "Exportar",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Carácter(es) inválido(s) en el nombre del archivo: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Exportar escaneos a PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "Todos los escaneos serán combinados en un único archivo PDF.",
"behavior": "Comportamiento",
"@behavior": {
"description": "Title of the settings concerning app beahvior"
},
"theme": "Tema",
"@theme": {
"description": "Title of the theme mode setting"
},
"clearCache": "Borrar caché",
"@clearCache": {
"description": "Title of the clear cache setting"
},
"freeBytes": "{byteString} libres",
"@freeBytes": {
"description": "Text shown for clear storage settings"
},
"calculatingDots": "Calculando...",
"@calculatingDots": {
"description": "Text shown when the byte size is still being calculated"
},
"freedDiskSpace": "{bytes} borrados del disco correctamente.",
"@freedDiskSpace": {
"description": "Message shown after clearing storage"
},
"uploadScansAsPdf": "Subir escaneos como PDF",
"@uploadScansAsPdf": {
"description": "Title of the setting which toggles whether scans are always uploaded as pdf"
},
"convertSinglePageScanToPdf": "Convertir siempre escaneos de una sola página a PDF antes de subirlos",
"@convertSinglePageScanToPdf": {
"description": "description of the upload scans as pdf setting"
},
"loginRequiredPermissionsHint": "El uso de Paperless Mobile requiere un conjunto mínimo de permisos de usuario de paperless-ngx desde la versión 1.14.0 en adelante. Por lo tanto, asegúrese de que el usuario que inicie sesión tenga permiso para ver otros usuarios (Usuario → Vista) y sus configuraciones (Ajustes de UI → Vista). Si no tiene estos permisos, contacte al administrador de su servidor de paperless-ngx.",
"@loginRequiredPermissionsHint": {
"description": "Hint shown on the login page informing the user of the required permissions to use the app."
},
"missingPermissions": "No tiene los permisos necesarios para realizar esta acción.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donar",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -864,5 +864,17 @@
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donations",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -67,7 +67,7 @@
"@startTyping": {},
"doYouReallyWantToDeleteThisView": "Czy na pewno chcesz usunąć ten widok?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Usuń widok ",
"deleteView": "Usuń widok {name}?",
"@deleteView": {},
"addedAt": "Dodano",
"@addedAt": {},
@@ -864,5 +864,17 @@
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -67,7 +67,7 @@
"@startTyping": {},
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Delete view ",
"deleteView": "",
"@deleteView": {},
"addedAt": "Added at",
"@addedAt": {},
@@ -864,5 +864,17 @@
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donate",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -67,7 +67,7 @@
"@startTyping": {},
"doYouReallyWantToDeleteThisView": "Bu görünümü gerçekten silmek istiyor musunuz?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Görünümü sil",
"deleteView": "Görünümü sil {name}?",
"@deleteView": {},
"addedAt": "Added at",
"@addedAt": {},
@@ -864,5 +864,17 @@
"missingPermissions": "You do not have the necessary permissions to perform this action.",
"@missingPermissions": {
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
},
"editView": "Edit View",
"@editView": {
"description": "Title of the edit saved view page"
},
"donate": "Donate",
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
}
}

View File

@@ -34,6 +34,7 @@ import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
@@ -42,6 +43,7 @@ import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart';
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
import 'package:paperless_mobile/routes/typed/shells/provider_shell_route.dart';
import 'package:paperless_mobile/routes/typed/shells/scaffold_shell_route.dart';
@@ -231,27 +233,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
navigatorKey: rootNavigatorKey,
builder: ProviderShellRoute(widget.apiFactory).build,
routes: [
// GoRoute(
// parentNavigatorKey: rootNavigatorKey,
// name: R.savedView,
// path: "/saved_view/:id",
// builder: (context, state) {
// return Placeholder(
// child: Text("Documents"),
// );
// },
// routes: [
// GoRoute(
// path: "create",
// name: R.createSavedView,
// builder: (context, state) {
// return Placeholder(
// child: Text("Documents"),
// );
// },
// ),
// ],
// ),
$savedViewsRoute,
StatefulShellRoute(
navigatorContainerBuilder: (context, navigationShell, children) {
return children[navigationShell.currentIndex];

View File

@@ -7,6 +7,7 @@ class R {
static const switchingAccounts = "switchingAccounts";
static const savedView = "savedView";
static const createSavedView = "createSavedView";
static const editSavedView = "editSavedView";
static const documentDetails = "documentDetails";
static const editDocument = "editDocument";
static const labels = "labels";

View File

@@ -2,8 +2,24 @@ import 'package:flutter/src/widgets/framework.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view/view/edit_saved_view_page.dart';
import 'package:paperless_mobile/routes/routes.dart';
@TypedGoRoute(path: "/saved-views", routes: [])
part 'saved_views_route.g.dart';
@TypedGoRoute<SavedViewsRoute>(
path: "/saved-views",
routes: [
TypedGoRoute<CreateSavedViewRoute>(
path: "create",
name: R.createSavedView,
),
TypedGoRoute<EditSavedViewRoute>(
path: "edit",
name: R.editSavedView,
),
],
)
class SavedViewsRoute extends GoRouteData {
const SavedViewsRoute();
}
@@ -14,12 +30,16 @@ class CreateSavedViewRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return AddSavedViewPage(
initialFilter: $extra,
);
return AddSavedViewPage(initialFilter: $extra);
}
}
class EditSavedViewRoute extends GoRouteData {
const EditSavedViewRoute();
final SavedView $extra;
const EditSavedViewRoute(this.$extra);
@override
Widget build(BuildContext context, GoRouterState state) {
return EditSavedViewPage(savedView: $extra);
}
}