feat: Update translations, finish saved views rework, some other fixes

This commit is contained in:
Anton Stubenbord
2023-09-22 00:46:24 +02:00
parent f3560f00ea
commit 18ab657932
55 changed files with 2049 additions and 1087 deletions

View File

@@ -10,7 +10,9 @@ import 'package:hive_flutter/adapters.dart';
/// [callback] to return and returns the calculated value. Closes the box after. /// [callback] to return and returns the calculated value. Closes the box after.
/// ///
Future<R?> withEncryptedBox<T, R>( Future<R?> withEncryptedBox<T, R>(
String name, FutureOr<R?> Function(Box<T> box) callback) async { String name,
FutureOr<R?> Function(Box<T> box) callback,
) async {
final key = await _getEncryptedBoxKey(); final key = await _getEncryptedBoxKey();
final box = await Hive.openBox<T>( final box = await Hive.openBox<T>(
name, name,
@@ -22,7 +24,11 @@ Future<R?> withEncryptedBox<T, R>(
} }
Future<Uint8List> _getEncryptedBoxKey() async { Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage(); const secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
if (!await secureStorage.containsKey(key: 'key')) { if (!await secureStorage.containsKey(key: 'key')) {
final key = Hive.generateSecureKey(); final key = Hive.generateSecureKey();

View File

@@ -1,8 +1,7 @@
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
part 'local_user_account.g.dart'; part 'local_user_account.g.dart';

View File

@@ -13,7 +13,6 @@ import 'package:paperless_mobile/features/document_bulk_action/view/widgets/full
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart'; import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -8,25 +8,26 @@ abstract class PersistentRepository<T> extends HydratedCubit<T> {
PersistentRepository(T initialState) : super(initialState); PersistentRepository(T initialState) : super(initialState);
void addListener( void addListener(
Object source, { Object subscriber, {
required void Function(T) onChanged, required void Function(T) onChanged,
}) { }) {
onChanged(state); onChanged(state);
_subscribers.putIfAbsent(source, () { _subscribers.putIfAbsent(subscriber, () {
return stream.listen((event) => onChanged(event)); return stream.listen((event) => onChanged(event));
}); });
} }
void removeListener(Object source) async { void removeListener(Object source) async {
await _subscribers[source]?.cancel(); _subscribers
_subscribers.remove(source); ..[source]?.cancel()
..remove(source);
} }
@override @override
Future<void> close() { Future<void> close() {
_subscribers.forEach((key, subscription) { for (final subscriber in _subscribers.values) {
subscription.cancel(); subscriber.cancel();
}); }
return super.close(); return super.close();
} }
} }

View File

@@ -54,25 +54,26 @@ String translateError(BuildContext context, ErrorCode code) {
ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions, ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions,
ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks, ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks,
ErrorCode.correspondentDeleteFailed => ErrorCode.correspondentDeleteFailed =>
"Could not delete correspondent, please try again.", //TODO: INTL S.of(context)!.couldNotDeleteCorrespondent,
ErrorCode.documentTypeDeleteFailed => ErrorCode.documentTypeDeleteFailed =>
"Could not delete document type, please try again.", S.of(context)!.couldNotDeleteDocumentType,
ErrorCode.tagDeleteFailed => "Could not delete tag, please try again.", ErrorCode.tagDeleteFailed => S.of(context)!.couldNotDeleteTag,
ErrorCode.correspondentUpdateFailed =>
"Could not update correspondent, please try again.",
ErrorCode.documentTypeUpdateFailed =>
"Could not update document type, please try again.",
ErrorCode.tagUpdateFailed => "Could not update tag, please try again.",
ErrorCode.storagePathDeleteFailed => ErrorCode.storagePathDeleteFailed =>
"Could not delete storage path, please try again.", S.of(context)!.couldNotDeleteStoragePath,
ErrorCode.correspondentUpdateFailed =>
S.of(context)!.couldNotUpdateCorrespondent,
ErrorCode.documentTypeUpdateFailed =>
S.of(context)!.couldNotUpdateDocumentType,
ErrorCode.tagUpdateFailed => S.of(context)!.couldNotUpdateTag,
ErrorCode.storagePathUpdateFailed => ErrorCode.storagePathUpdateFailed =>
"Could not update storage path, please try again.", S.of(context)!.couldNotUpdateStoragePath,
ErrorCode.serverInformationLoadFailed => ErrorCode.serverInformationLoadFailed =>
"Could not load server information.", S.of(context)!.couldNotLoadServerInformation,
ErrorCode.serverStatisticsLoadFailed => "Could not load server statistics.", ErrorCode.serverStatisticsLoadFailed =>
ErrorCode.uiSettingsLoadFailed => "Could not load UI settings", S.of(context)!.couldNotLoadStatistics,
ErrorCode.loadTasksError => "Could not load tasks.", ErrorCode.uiSettingsLoadFailed => S.of(context)!.couldNotLoadUISettings,
ErrorCode.userNotFound => "User could not be found.", ErrorCode.loadTasksError => S.of(context)!.couldNotLoadTasks,
ErrorCode.updateSavedViewError => "Could not update saved view.", ErrorCode.userNotFound => S.of(context)!.userNotFound,
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
}; };
} }

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/unsaved_changes_warning_dialog.dart';
class PopWithUnsavedChanges extends StatelessWidget {
final bool Function() hasChangesPredicate;
final Widget child;
const PopWithUnsavedChanges({
super.key,
required this.hasChangesPredicate,
required this.child,
});
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (hasChangesPredicate()) {
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) => const UnsavedChangesWarningDialog(),
) ??
false;
return shouldPop;
}
return true;
},
child: child,
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class UnsavedChangesWarningDialog extends StatelessWidget {
const UnsavedChangesWarningDialog({super.key});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Discard changes?"),
content: Text(
"You have unsaved changes. Do you want to continue without saving? Your changes will be discarded.",
),
actions: [
DialogCancelButton(),
DialogConfirmButton(
label: S.of(context)!.continueLabel,
),
],
);
}
}

View File

@@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class EmptyState extends StatelessWidget {
final String title;
final String subtitle;
final Widget? bottomChild;
const EmptyState({
Key? key,
required this.title,
required this.subtitle,
this.bottomChild,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: size.height / 3,
width: size.width / 3,
child: SvgPicture.asset("assets/images/empty-state.svg"),
),
Column(
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
subtitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
if (bottomChild != null) ...[bottomChild!] else ...[]
],
);
}
}

View File

@@ -15,10 +15,8 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart'; import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -5,11 +5,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
@@ -19,7 +19,6 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
class DocumentEditPage extends StatefulWidget { class DocumentEditPage extends StatefulWidget {
@@ -46,253 +45,257 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser; final currentUser = context.watch<LocalUserAccount>().paperlessUser;
return BlocBuilder<DocumentEditCubit, DocumentEditState>( return PopWithUnsavedChanges(
builder: (context, state) { hasChangesPredicate: () => _formKey.currentState?.isDirty ?? false,
final filteredSuggestions = state.suggestions?.documentDifference( child: BlocBuilder<DocumentEditCubit, DocumentEditState>(
context.read<DocumentEditCubit>().state.document); builder: (context, state) {
return DefaultTabController( final filteredSuggestions = state.suggestions?.documentDifference(
length: 2, context.read<DocumentEditCubit>().state.document);
child: Scaffold( return DefaultTabController(
resizeToAvoidBottomInset: false, length: 2,
floatingActionButton: FloatingActionButton.extended( child: Scaffold(
heroTag: "fab_document_edit", resizeToAvoidBottomInset: false,
onPressed: () => _onSubmit(state.document), floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.save), heroTag: "fab_document_edit",
label: Text(S.of(context)!.saveChanges), onPressed: () => _onSubmit(state.document),
), icon: const Icon(Icons.save),
appBar: AppBar( label: Text(S.of(context)!.saveChanges),
title: Text(S.of(context)!.editDocument),
bottom: TabBar(
tabs: [
Tab(
text: S.of(context)!.overview,
),
Tab(
text: S.of(context)!.content,
)
],
), ),
), appBar: AppBar(
extendBody: true, title: Text(S.of(context)!.editDocument),
body: Padding( bottom: TabBar(
padding: EdgeInsets.only( tabs: [
bottom: MediaQuery.of(context).viewInsets.bottom, Tab(text: S.of(context)!.overview),
top: 8, Tab(text: S.of(context)!.content)
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,
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(
initialName: initialValue,
),
),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options: context
.watch<DocumentEditCubit>()
.state
.correspondents,
initialValue:
state.document.correspondent != null
? IdQueryParameter.fromId(
state.document.correspondent!)
: const IdQueryParameter.unset(),
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true,
canCreateNewLabel:
currentUser.canCreateCorrespondents,
),
if (filteredSuggestions
?.hasSuggestedCorrespondents ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.correspondents[itemData]!.name),
onPressed: () {
_formKey.currentState
?.fields[fkCorrespondent]
?.didChange(
IdQueryParameter.fromId(itemData),
);
},
),
),
],
).padded(),
// DocumentType form field
if (currentUser.canViewDocumentTypes)
Column(
children: [
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (currentInput) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
canCreateNewLabel:
currentUser.canCreateDocumentTypes,
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue:
state.document.documentType != null
? IdQueryParameter.fromId(
state.document.documentType!)
: const IdQueryParameter.unset(),
options: state.documentTypes,
name: _DocumentEditPageState.fkDocumentType,
prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
),
if (filteredSuggestions
?.hasSuggestedDocumentTypes ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.documentTypes[itemData]!.name),
onPressed: () => _formKey
.currentState?.fields[fkDocumentType]
?.didChange(
IdQueryParameter.fromId(itemData),
),
),
),
],
).padded(),
// StoragePath form field
if (currentUser.canViewStoragePaths)
Column(
children: [
LabelFormField<StoragePath>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(
initialName: initialValue),
),
canCreateNewLabel:
currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
initialValue:
state.document.storagePath != null
? IdQueryParameter.fromId(
state.document.storagePath!)
: const IdQueryParameter.unset(),
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
allowSelectUnassigned: true,
),
],
).padded(),
// Tag form field
if (currentUser.canViewTags)
TagsFormField(
options: state.tags,
name: fkTags,
allowOnlySelection: true,
allowCreation: true,
allowExclude: false,
initialValue: TagsQuery.ids(
include: state.document.tags.toList(),
),
).padded(),
if (filteredSuggestions?.tags
.toSet()
.difference(state.document.tags.toSet())
.isNotEmpty ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
(filteredSuggestions?.tags.toSet() ?? {}),
itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!;
return ActionChip(
label: Text(
tag.name,
style: TextStyle(color: tag.textColor),
),
backgroundColor: tag.color,
onPressed: () {
final currentTags = _formKey.currentState
?.fields[fkTags]?.value as TagsQuery;
_formKey.currentState?.fields[fkTags]
?.didChange(
currentTags.maybeWhen(
ids: (include, exclude) =>
TagsQuery.ids(
include: [...include, itemData],
exclude: exclude),
orElse: () =>
TagsQuery.ids(include: [itemData]),
),
);
},
);
},
),
// Prevent tags from being hidden by fab
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),
],
),
),
], ],
), ),
), ),
)), 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,
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(
initialName: initialValue,
),
),
addLabelText:
S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options: context
.watch<DocumentEditCubit>()
.state
.correspondents,
initialValue:
state.document.correspondent != null
? IdQueryParameter.fromId(
state.document.correspondent!)
: const IdQueryParameter.unset(),
name: fkCorrespondent,
prefixIcon:
const Icon(Icons.person_outlined),
allowSelectUnassigned: true,
canCreateNewLabel:
currentUser.canCreateCorrespondents,
),
if (filteredSuggestions
?.hasSuggestedCorrespondents ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(state
.correspondents[itemData]!.name),
onPressed: () {
_formKey.currentState
?.fields[fkCorrespondent]
?.didChange(
IdQueryParameter.fromId(itemData),
);
},
),
),
],
).padded(),
// DocumentType form field
if (currentUser.canViewDocumentTypes)
Column(
children: [
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (currentInput) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
canCreateNewLabel:
currentUser.canCreateDocumentTypes,
addLabelText:
S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue:
state.document.documentType != null
? IdQueryParameter.fromId(
state.document.documentType!)
: const IdQueryParameter.unset(),
options: state.documentTypes,
name: _DocumentEditPageState.fkDocumentType,
prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
),
if (filteredSuggestions
?.hasSuggestedDocumentTypes ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(state
.documentTypes[itemData]!.name),
onPressed: () => _formKey.currentState
?.fields[fkDocumentType]
?.didChange(
IdQueryParameter.fromId(itemData),
),
),
),
],
).padded(),
// StoragePath form field
if (currentUser.canViewStoragePaths)
Column(
children: [
LabelFormField<StoragePath>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(
initialName: initialValue),
),
canCreateNewLabel:
currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
initialValue:
state.document.storagePath != null
? IdQueryParameter.fromId(
state.document.storagePath!)
: const IdQueryParameter.unset(),
name: fkStoragePath,
prefixIcon:
const Icon(Icons.folder_outlined),
allowSelectUnassigned: true,
),
],
).padded(),
// Tag form field
if (currentUser.canViewTags)
TagsFormField(
options: state.tags,
name: fkTags,
allowOnlySelection: true,
allowCreation: true,
allowExclude: false,
initialValue: TagsQuery.ids(
include: state.document.tags.toList(),
),
).padded(),
if (filteredSuggestions?.tags
.toSet()
.difference(state.document.tags.toSet())
.isNotEmpty ??
false)
_buildSuggestionsSkeleton<int>(
suggestions:
(filteredSuggestions?.tags.toSet() ?? {}),
itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!;
return ActionChip(
label: Text(
tag.name,
style: TextStyle(color: tag.textColor),
),
backgroundColor: tag.color,
onPressed: () {
final currentTags = _formKey.currentState
?.fields[fkTags]?.value as TagsQuery;
_formKey.currentState?.fields[fkTags]
?.didChange(
currentTags.maybeWhen(
ids: (include, exclude) =>
TagsQuery.ids(include: [
...include,
itemData
], exclude: exclude),
orElse: () => TagsQuery.ids(
include: [itemData]),
),
);
},
);
},
),
// Prevent tags from being hidden by fab
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),
],
),
),
],
),
),
)),
);
},
),
); );
} }

View File

@@ -14,7 +14,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/service/file_description.dart'; import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
@@ -58,6 +57,7 @@ class _ScannerPageState extends State<ScannerPage>
return BlocBuilder<DocumentScannerCubit, List<File>>( return BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) { builder: (context, state) {
return SafeArea( return SafeArea(
top: true,
child: Scaffold( child: Scaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(

View File

@@ -24,8 +24,10 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
this.api, this.api,
this.notifier, this.notifier,
this._userAppState, this._userAppState,
) : super(DocumentSearchState( ) : super(
searchHistory: _userAppState.documentSearchHistory)) { DocumentSearchState(
searchHistory: _userAppState.documentSearchHistory),
) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
@@ -34,22 +36,25 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
} }
Future<void> search(String query) async { Future<void> search(String query) async {
emit(state.copyWith( final normalizedQuery = query.trim();
isLoading: true, emit(
suggestions: [], state.copyWith(
view: SearchView.results, isLoading: true,
)); suggestions: [],
view: SearchView.results,
),
);
final searchFilter = DocumentFilter( final searchFilter = DocumentFilter(
query: TextQuery.extended(query), query: TextQuery.extended(normalizedQuery),
); );
await updateFilter(filter: searchFilter); await updateFilter(filter: searchFilter);
emit( emit(
state.copyWith( state.copyWith(
searchHistory: [ searchHistory: [
query, normalizedQuery,
...state.searchHistory ...state.searchHistory
.whereNot((previousQuery) => previousQuery == query) .whereNot((previousQuery) => previousQuery == normalizedQuery)
], ],
), ),
); );

View File

@@ -21,75 +21,70 @@ class DocumentSearchBar extends StatefulWidget {
class _DocumentSearchBarState extends State<DocumentSearchBar> { class _DocumentSearchBarState extends State<DocumentSearchBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return OpenContainer(
margin: EdgeInsets.only(top: 8), transitionDuration: const Duration(milliseconds: 200),
child: OpenContainer( transitionType: ContainerTransitionType.fadeThrough,
transitionDuration: const Duration(milliseconds: 200), closedElevation: 1,
transitionType: ContainerTransitionType.fadeThrough, middleColor: Theme.of(context).colorScheme.surfaceVariant,
closedElevation: 1, openColor: Theme.of(context).colorScheme.background,
middleColor: Theme.of(context).colorScheme.surfaceVariant, closedColor: Theme.of(context).colorScheme.surfaceVariant,
openColor: Theme.of(context).colorScheme.background, closedShape: RoundedRectangleBorder(
closedColor: Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(56),
closedShape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(56), closedBuilder: (_, action) {
), return InkWell(
closedBuilder: (_, action) { onTap: action,
return InkWell( child: ConstrainedBox(
onTap: action, constraints: const BoxConstraints(
child: ConstrainedBox( maxWidth: 720,
constraints: const BoxConstraints( minWidth: 360,
maxWidth: 720, maxHeight: 56,
minWidth: 360, minHeight: 48,
maxHeight: 56, ),
minHeight: 48, child: Row(
), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Flexible(
children: [ child: Padding(
Flexible( padding: const EdgeInsets.symmetric(horizontal: 8),
child: Padding( child: Row(
padding: const EdgeInsets.symmetric(horizontal: 8), crossAxisAlignment: CrossAxisAlignment.center,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.center, IconButton(
children: [ icon: const Icon(Icons.menu),
IconButton( onPressed: Scaffold.of(context).openDrawer,
icon: const Icon(Icons.menu), ),
onPressed: Scaffold.of(context).openDrawer, Flexible(
child: Text(
S.of(context)!.searchDocuments,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).hintColor,
),
), ),
Flexible( ),
child: Text( ],
S.of(context)!.searchDocuments,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).hintColor,
),
),
),
],
),
), ),
), ),
_buildUserAvatar(context), ),
], _buildUserAvatar(context),
), ],
), ),
); ),
}, );
openBuilder: (_, action) { },
return Provider( openBuilder: (_, action) {
create: (_) => DocumentSearchCubit( return Provider(
context.read(), create: (_) => DocumentSearchCubit(
context.read(), context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState) context.read(),
.get(context.read<LocalUserAccount>().id)!, Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
), .get(context.read<LocalUserAccount>().id)!,
child: const DocumentSearchPage(), ),
); child: const DocumentSearchPage(),
}, );
), },
); );
} }

View File

@@ -4,8 +4,6 @@ import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart'; import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
@@ -188,7 +186,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
children: [ children: [
Text( Text(
S.of(context)!.results, S.of(context)!.results,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.labelMedium,
), ),
BlocBuilder<DocumentSearchCubit, DocumentSearchState>( BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
builder: (context, state) { builder: (context, state) {
@@ -200,15 +198,15 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
}, },
) )
], ],
).padded(); ).paddedLTRB(16, 8, 8, 8);
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter(child: header), SliverToBoxAdapter(child: header),
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Center( child: Center(
child: Text(S.of(context)!.noMatchesFound), child: Text(S.of(context)!.noDocumentsFound),
), ).paddedOnly(top: 8),
) )
else else
SliverAdaptiveDocumentsView( SliverAdaptiveDocumentsView(

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
@@ -8,6 +9,7 @@ import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dar
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sliver_tools/sliver_tools.dart';
class SliverSearchBar extends StatelessWidget { class SliverSearchBar extends StatelessWidget {
final bool floating; final bool floating;
@@ -22,14 +24,13 @@ class SliverSearchBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) { if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) {
return SliverAppBar( return SliverAppBar(
toolbarHeight: kToolbarHeight, titleSpacing: 8,
flexibleSpace: Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: const DocumentSearchBar(),
),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: DocumentSearchBar(),
); );
} else { } else {
return SliverAppBar( return SliverAppBar(

View File

@@ -1,11 +1,11 @@
import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:defer_pointer/defer_pointer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
@@ -18,8 +18,8 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/confi
import 'package:paperless_mobile/features/documents/view/widgets/selection/document_selection_sliver_app_bar.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/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -43,45 +43,58 @@ class DocumentsPage extends StatefulWidget {
State<DocumentsPage> createState() => _DocumentsPageState(); State<DocumentsPage> createState() => _DocumentsPageState();
} }
class _DocumentsPageState extends State<DocumentsPage> class _DocumentsPageState extends State<DocumentsPage> {
with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle = final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle(); SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle savedViewsHandle = final SliverOverlapAbsorberHandle savedViewsHandle =
SliverOverlapAbsorberHandle(); SliverOverlapAbsorberHandle();
late final TabController _tabController;
int _currentTab = 0; final _nestedScrollViewKey = GlobalKey<NestedScrollViewState>();
final _savedViewsExpansionController = ExpansionTileController(); final _savedViewsExpansionController = ExpansionTileController();
bool _showExtendedFab = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final showSavedViews = WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<LocalUserAccount>().paperlessUser.canViewSavedViews; _nestedScrollViewKey.currentState!.innerController
_tabController = TabController( .addListener(_scrollExtentChangedListener);
length: showSavedViews ? 2 : 1, });
vsync: this,
);
// Future.wait([
// context.read<DocumentsCubit>().reload(),
// context.read<SavedViewCubit>().reload(),
// ]).onError<PaperlessApiException>(
// (error, stackTrace) {
// showErrorMessage(context, error, stackTrace);
// return [];
// },
// );
_tabController.addListener(_tabChangesListener);
} }
void _tabChangesListener() { Future<void> _reloadData() async {
setState(() => _currentTab = _tabController.index); try {
await Future.wait([
context.read<DocumentsCubit>().reload(),
context.read<SavedViewCubit>().reload(),
context.read<LabelCubit>().reload(),
]);
} catch (error, stackTrace) {
showGenericError(context, error, stackTrace);
}
}
void _scrollExtentChangedListener() {
const threshold = 400;
final offset =
_nestedScrollViewKey.currentState!.innerController.position.pixels;
if (offset < threshold && _showExtendedFab == false) {
setState(() {
_showExtendedFab = true;
});
} else if (offset >= threshold && _showExtendedFab == true) {
setState(() {
_showExtendedFab = false;
});
}
} }
@override @override
void dispose() { void dispose() {
_tabController.dispose(); _nestedScrollViewKey.currentState?.innerController
.removeListener(_scrollExtentChangedListener);
super.dispose(); super.dispose();
} }
@@ -109,11 +122,7 @@ class _DocumentsPageState extends State<DocumentsPage>
previous != ConnectivityState.connected && previous != ConnectivityState.connected &&
current == ConnectivityState.connected, current == ConnectivityState.connected,
listener: (context, state) { listener: (context, state) {
try { _reloadData();
context.read<DocumentsCubit>().reload();
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}, },
builder: (context, connectivityState) { builder: (context, connectivityState) {
return SafeArea( return SafeArea(
@@ -122,59 +131,104 @@ class _DocumentsPageState extends State<DocumentsPage>
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>( floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
final show = state.selection.isEmpty; final show = state.selection.isEmpty;
final canReset = state.filter.appliedFiltersCount > 0; final canReset = state.filter.appliedFiltersCount > 0;
return AnimatedScale( if (show) {
scale: show ? 1 : 0, return Column(
duration: const Duration(milliseconds: 200),
curve: Curves.easeIn,
child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (canReset) DeferredPointerHandler(
Padding( child: Stack(
padding: const EdgeInsets.all(8.0), clipBehavior: Clip.none,
child: FloatingActionButton.small( children: [
heroTag: "fab_documents_page_reset_filter", FloatingActionButton.extended(
backgroundColor: Theme.of(context) extendedPadding: _showExtendedFab
.colorScheme ? null
.onPrimaryContainer, : const EdgeInsets.symmetric(
onPressed: () { horizontal: 16),
_onResetFilter(); heroTag: "fab_documents_page_filter",
}, label: AnimatedSwitcher(
child: Icon( duration: const Duration(milliseconds: 150),
Icons.refresh, transitionBuilder: (child, animation) {
color: Theme.of(context) return FadeTransition(
.colorScheme opacity: animation,
.primaryContainer, child: SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
child: child,
),
);
},
child: _showExtendedFab
? Row(
children: [
const Icon(
Icons.filter_alt_outlined,
),
const SizedBox(width: 8),
Text(
S.of(context)!.filterDocuments,
),
],
)
: const Icon(Icons.filter_alt_outlined),
),
onPressed: _openDocumentFilter,
), ),
), if (canReset)
Positioned(
top: -20,
right: -8,
child: DeferPointer(
paintOnTop: true,
child: Material(
color:
Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
HapticFeedback.mediumImpact();
_onResetFilter();
},
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
if (_showExtendedFab)
Text(
"Reset (${state.filter.appliedFiltersCount})",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onError,
),
).padded()
else
Icon(
Icons.replay,
color: Theme.of(context)
.colorScheme
.onError,
).padded(4),
],
),
),
),
),
),
],
), ),
b.Badge(
position: b.BadgePosition.topEnd(top: -12, end: -6),
showBadge: appliedFiltersCount > 0,
badgeContent: Text(
'$appliedFiltersCount',
style: const TextStyle(
color: Colors.white,
),
),
animationType: b.BadgeAnimationType.fade,
badgeColor: Colors.red,
child: AnimatedSwitcher(
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,
);
})),
), ),
], ],
), );
); } else {
return const SizedBox.shrink();
}
}, },
), ),
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
@@ -190,94 +244,41 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
return true; return true;
}, },
child: Stack( child: NestedScrollView(
children: [ key: _nestedScrollViewKey,
NestedScrollView( floatHeaderSlivers: true,
floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) => [
headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber(
SliverOverlapAbsorber( handle: searchBarHandle,
handle: searchBarHandle, sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
sliver: BlocBuilder<DocumentsCubit, DocumentsState>( builder: (context, state) {
builder: (context, state) { if (state.selection.isEmpty) {
if (state.selection.isEmpty) { return SliverSearchBar(
return SliverSearchBar( floating: true,
floating: true, titleText: S.of(context)!.documents,
titleText: S.of(context)!.documents, );
); } else {
} else { return DocumentSelectionSliverAppBar(
return DocumentSelectionSliverAppBar( state: state,
state: state, );
);
}
},
),
),
SliverOverlapAbsorber(
handle: savedViewsHandle,
sliver: SliverPinnedHeader(
child: Material(
child: _buildViewActions(),
elevation: 4,
),
),
),
// SliverOverlapAbsorber(
// handle: tabBarHandle,
// sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
// builder: (context, state) {
// if (state.selection.isNotEmpty) {
// return const SliverToBoxAdapter(
// child: SizedBox.shrink(),
// );
// }
// return SliverPersistentHeader(
// pinned: true,
// delegate:
// CustomizableSliverPersistentHeaderDelegate(
// minExtent: kTextTabBarHeight,
// maxExtent: kTextTabBarHeight,
// child: ColoredTabBar(
// tabBar: TabBar(
// controller: _tabController,
// tabs: [
// Tab(text: S.of(context)!.documents),
// if (context
// .watch<LocalUserAccount>()
// .paperlessUser
// .canViewSavedViews)
// Tab(text: S.of(context)!.views),
// ],
// ),
// ),
// ),
// );
// },
// ),
// ),
],
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
} }
final desiredTab =
(metrics.pixels / metrics.maxScrollExtent)
.round();
if (metrics.axis == Axis.horizontal &&
_currentTab != desiredTab) {
setState(() => _currentTab = desiredTab);
}
return false;
}, },
child: _buildDocumentsTab( ),
connectivityState, ),
context, SliverOverlapAbsorber(
handle: savedViewsHandle,
sliver: SliverPinnedHeader(
child: Material(
child: _buildViewActions(),
elevation: 2,
), ),
), ),
), ),
_buildSavedViewChangedIndicator(),
], ],
body: _buildDocumentsTab(
connectivityState,
context,
),
), ),
), ),
), ),
@@ -287,82 +288,6 @@ class _DocumentsPageState extends State<DocumentsPage>
); );
} }
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,
),
),
),
),
),
),
);
},
);
}
// 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( Widget _buildDocumentsTab(
ConnectivityState connectivityState, ConnectivityState connectivityState,
BuildContext context, BuildContext context,
@@ -376,12 +301,11 @@ class _DocumentsPageState extends State<DocumentsPage>
_savedViewsExpansionController.collapse(); _savedViewsExpansionController.collapse();
} }
final currState = context.read<DocumentsCubit>().state;
final max = notification.metrics.maxScrollExtent; final max = notification.metrics.maxScrollExtent;
final currentState = context.read<DocumentsCubit>().state;
if (max == 0 || if (max == 0 ||
_currentTab != 0 || currentState.isLoading ||
currState.isLoading || currentState.isLastPageLoaded) {
currState.isLastPageLoaded) {
return false; return false;
} }
@@ -402,7 +326,7 @@ class _DocumentsPageState extends State<DocumentsPage>
}, },
child: RefreshIndicator( child: RefreshIndicator(
edgeOffset: kTextTabBarHeight + 2, edgeOffset: kTextTabBarHeight + 2,
onRefresh: _onReloadDocuments, onRefresh: _reloadData,
notificationPredicate: (_) => connectivityState.isConnected, notificationPredicate: (_) => connectivityState.isConnected,
child: CustomScrollView( child: CustomScrollView(
key: const PageStorageKey<String>("documents"), key: const PageStorageKey<String>("documents"),
@@ -428,8 +352,8 @@ class _DocumentsPageState extends State<DocumentsPage>
}, },
onUpdateView: (view) async { onUpdateView: (view) async {
await context.read<SavedViewCubit>().update(view); await context.read<SavedViewCubit>().update(view);
showSnackBar(context, showSnackBar(
"Saved view successfully updated."); //TODO: INTL context, S.of(context)!.savedViewSuccessfullyUpdated);
}, },
onDeleteView: (view) async { onDeleteView: (view) async {
HapticFeedback.mediumImpact(); HapticFeedback.mediumImpact();
@@ -496,7 +420,7 @@ class _DocumentsPageState extends State<DocumentsPage>
return BlocBuilder<DocumentsCubit, DocumentsState>( return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
return Container( return Container(
padding: EdgeInsets.all(4), padding: const EdgeInsets.all(4),
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -515,18 +439,6 @@ class _DocumentsPageState extends State<DocumentsPage>
); );
} }
void _onCreateSavedView(DocumentFilter filter) async {
//TODO: Implement
// final newView = await pushAddSavedViewRoute(context, filter: filter);
// if (newView != null) {
// try {
// await context.read<SavedViewCubit>().add(newView);
// } on PaperlessApiException catch (error, stackTrace) {
// showErrorMessage(context, error, stackTrace);
// }
// }
}
void _openDocumentFilter() async { void _openDocumentFilter() async {
final draggableSheetController = DraggableScrollableController(); final draggableSheetController = DraggableScrollableController();
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>( final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
@@ -717,66 +629,46 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
} }
Future<void> _onReloadDocuments() async { ///
try { /// Resets the current filter and scrolls all the way to the top of the view.
// We do not await here on purpose so we can show a linear progress indicator below the app bar. /// If a saved view is currently selected and the filter has changed,
await Future.wait([ /// the user will be shown a dialog informing them about the changes.
context.read<DocumentsCubit>().reload(), /// The user can then decide whether to abort the reset or to continue and discard the changes.
context.read<SavedViewCubit>().reload(),
]);
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
Future<void> _onResetFilter() async { Future<void> _onResetFilter() async {
final cubit = context.read<DocumentsCubit>(); final cubit = context.read<DocumentsCubit>();
final savedViewCubit = context.read<SavedViewCubit>(); final savedViewCubit = context.read<SavedViewCubit>();
final activeView = savedViewCubit.state.maybeMap(
void toTop() async {
await _nestedScrollViewKey.currentState?.outerController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
final activeView = savedViewCubit.state.mapOrNull(
loaded: (state) { loaded: (state) {
if (cubit.state.filter.selectedView != null) { if (cubit.state.filter.selectedView != null) {
return state.savedViews[cubit.state.filter.selectedView!]; return state.savedViews[cubit.state.filter.selectedView!];
} }
return null; return null;
}, },
orElse: () => null,
); );
final viewHasChanged = activeView != null && final viewHasChanged = activeView != null &&
activeView.toDocumentFilter() != cubit.state.filter; activeView.toDocumentFilter() != cubit.state.filter;
if (viewHasChanged) { if (viewHasChanged) {
final discardChanges = await showDialog( final discardChanges = await showDialog<bool>(
context: context, context: context,
builder: (context) => const SavedViewChangedDialog(), builder: (context) => const SavedViewChangedDialog(),
); ) ??
if (discardChanges == true) { false;
if (discardChanges) {
cubit.resetFilter(); cubit.resetFilter();
// Reset toTop();
} else if (discardChanges == false) {
_updateCurrentSavedView();
} }
} else { } else {
cubit.resetFilter(); cubit.resetFilter();
toTop();
} }
} }
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

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/empty_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -8,6 +8,7 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class DocumentsEmptyState extends StatelessWidget { class DocumentsEmptyState extends StatelessWidget {
final DocumentPagingState state; final DocumentPagingState state;
final VoidCallback? onReset; final VoidCallback? onReset;
const DocumentsEmptyState({ const DocumentsEmptyState({
Key? key, Key? key,
required this.state, required this.state,
@@ -17,18 +18,24 @@ class DocumentsEmptyState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: EmptyState( child: Column(
title: S.of(context)!.oops, children: [
subtitle: S.of(context)!.thereSeemsToBeNothingHere, Text(
bottomChild: state.filter != DocumentFilter.initial && onReset != null S.of(context)!.noDocumentsFound,
? TextButton( style: Theme.of(context).textTheme.titleSmall,
onPressed: onReset, ),
child: Text( if (state.filter != DocumentFilter.initial && onReset != null)
S.of(context)!.resetFilter, TextButton(
), onPressed: () {
).padded() HapticFeedback.mediumImpact();
: null, onReset!();
), },
child: Text(
S.of(context)!.resetFilter,
),
).padded(),
],
).padded(24),
); );
} }
} }

View File

@@ -9,19 +9,11 @@ class SavedViewChangedDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text("Discard changes?"), //TODO: INTL title: Text(S.of(context)!.discardChanges),
content: Text( content: Text(S.of(context)!.savedViewChangedDialogContent),
"Some filters of the currently active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?", //TODO: INTL
),
actionsOverflowButtonSpacing: 8, actionsOverflowButtonSpacing: 8,
actions: [ actions: [
const DialogCancelButton(), const DialogCancelButton(),
// TextButton(
// child: Text(S.of(context)!.saveChanges),
// onPressed: () {
// Navigator.pop(context, false);
// },
// ),
DialogConfirmButton( DialogConfirmButton(
label: S.of(context)!.resetFilter, label: S.of(context)!.resetFilter,
style: DialogConfirmButtonStyle.danger, style: DialogConfirmButtonStyle.danger,

View File

@@ -3,7 +3,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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'; import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
class SavedViewChip extends StatefulWidget { class SavedViewChip extends StatefulWidget {
@@ -102,7 +101,6 @@ class _SavedViewChipState extends State<SavedViewChip>
_buildLabel(context, effectiveForegroundColor) _buildLabel(context, effectiveForegroundColor)
.paddedSymmetrically( .paddedSymmetrically(
horizontal: 12, horizontal: 12,
vertical: 0,
), ),
], ],
).paddedOnly(left: 8), ).paddedOnly(left: 8),
@@ -120,6 +118,7 @@ class _SavedViewChipState extends State<SavedViewChip>
Widget _buildTrailing(Color effectiveForegroundColor) { Widget _buildTrailing(Color effectiveForegroundColor) {
return IconButton( return IconButton(
padding: EdgeInsets.zero,
icon: AnimatedBuilder( icon: AnimatedBuilder(
animation: _animation, animation: _animation,
builder: (context, child) { builder: (context, child) {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/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/features/saved_view/cubit/saved_view_cubit.dart';
@@ -46,99 +46,185 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PageStorage( return BlocBuilder<SavedViewCubit, SavedViewState>(
bucket: PageStorageBucket(), builder: (context, state) {
child: ExpansionTile( final selectedView = state.mapOrNull(
controller: widget.controller, loaded: (value) {
tilePadding: const EdgeInsets.only(left: 8), if (widget.filter.selectedView != null) {
trailing: RotationTransition( return value.savedViews[widget.filter.selectedView!];
turns: _animation, }
child: const Icon(Icons.expand_more), },
).paddedOnly(right: 8), );
onExpansionChanged: (isExpanded) { final selectedViewHasChanged = selectedView != null &&
if (isExpanded) { selectedView.toDocumentFilter() != widget.filter;
_animationController.forward(); return PageStorage(
} else { bucket: PageStorageBucket(),
_animationController.reverse().then((value) => setState(() {})); child: ExpansionTile(
} controller: widget.controller,
}, tilePadding: const EdgeInsets.only(left: 8),
title: Text( trailing: RotationTransition(
S.of(context)!.views, turns: _animation,
style: Theme.of(context).textTheme.labelLarge, child: const Icon(Icons.expand_more),
), ).paddedOnly(right: 8),
leading: Icon( onExpansionChanged: (isExpanded) {
Icons.saved_search, if (isExpanded) {
color: Theme.of(context).colorScheme.primary, _animationController.forward();
).padded(), } else {
expandedCrossAxisAlignment: CrossAxisAlignment.start, _animationController.reverse().then((value) => setState(() {}));
children: [ }
BlocBuilder<SavedViewCubit, SavedViewState>( },
builder: (context, state) { title: Row(
return state.map( mainAxisAlignment: MainAxisAlignment.spaceBetween,
initial: (_) => const Placeholder(), children: [
loading: (_) => const Placeholder(), Flexible(
loaded: (value) { child: Column(
if (value.savedViews.isEmpty) { crossAxisAlignment: CrossAxisAlignment.start,
return Text(S.of(context)!.noItemsFound) children: [
.paddedOnly(left: 16); Text(
} S.of(context)!.views,
return Container( style: Theme.of(context).textTheme.labelLarge,
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),
),
],
), ),
if (selectedView != null)
Text(
selectedView.name,
style:
Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.5),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
AnimatedScale(
scale: selectedViewHasChanged ? 1 : 0,
duration: const Duration(milliseconds: 150),
child: TextButton(
onPressed: () {
final newView = selectedView!.copyWith(
filterRules: FilterRule.fromFilter(widget.filter),
);
widget.onUpdateView(newView);
},
child: Text(S.of(context)!.saveChanges),
),
)
],
),
leading: Icon(
Icons.saved_search,
color: Theme.of(context).colorScheme.primary,
).padded(),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
state
.maybeMap(
loaded: (value) {
if (value.savedViews.isEmpty) {
return Text(S.of(context)!.noItemsFound)
.paddedOnly(left: 16);
}
return SizedBox(
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),
),
],
),
),
);
},
error: (_) => Text(S.of(context)!.couldNotLoadSavedViews)
.paddedOnly(left: 16),
orElse: _buildLoadingState,
)
.paddedOnly(top: 16),
Align(
alignment: Alignment.centerRight,
child: Tooltip(
message: S.of(context)!.createFromCurrentFilter,
child: TextButton.icon(
onPressed: () {
CreateSavedViewRoute(widget.filter).push(context);
},
icon: const Icon(Icons.add),
label: Text(S.of(context)!.newView),
),
).padded(4),
),
],
),
);
},
);
}
Widget _buildLoadingState() {
return Container(
margin: const EdgeInsets.only(top: 16),
height: kMinInteractiveDimension,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) => true,
child: ShimmerPlaceholder(
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: [
const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
SliverList.separated(
itemBuilder: (context, index) {
return Container(
width: 130,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white,
), ),
); );
}, },
error: (_) => const Placeholder(), separatorBuilder: (context, index) => const SizedBox(width: 8),
);
},
),
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), const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
],
), ),
], ),
), ),
); );
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';

View File

@@ -2,15 +2,16 @@ import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
class EditLabelPage<T extends Label> extends StatelessWidget { class EditLabelPage<T extends Label> extends StatelessWidget {
@@ -56,8 +57,9 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
final Future<T> Function(BuildContext context, T label) onSubmit; final Future<T> Function(BuildContext context, T label) onSubmit;
final Future<void> Function(BuildContext context, T label) onDelete; final Future<void> Function(BuildContext context, T label) onDelete;
final bool canDelete; final bool canDelete;
final _formKey = GlobalKey<FormBuilderState>();
const EditLabelForm({ EditLabelForm({
super.key, super.key,
required this.label, required this.label,
required this.fromJsonT, required this.fromJsonT,
@@ -69,26 +71,32 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopWithUnsavedChanges(
appBar: AppBar( hasChangesPredicate: () {
title: Text(S.of(context)!.edit), return _formKey.currentState?.isDirty ?? false;
actions: [ },
IconButton( child: Scaffold(
onPressed: canDelete ? () => _onDelete(context) : null, appBar: AppBar(
icon: const Icon(Icons.delete), title: Text(S.of(context)!.edit),
), actions: [
], IconButton(
), onPressed: canDelete ? () => _onDelete(context) : null,
body: LabelForm<T>( icon: const Icon(Icons.delete),
autofocusNameField: false, ),
initialValue: label, ],
fromJsonT: fromJsonT, ),
submitButtonConfig: SubmitButtonConfig<T>( body: LabelForm<T>(
icon: const Icon(Icons.save), formKey: _formKey,
label: Text(S.of(context)!.saveChanges), autofocusNameField: false,
onSubmit: (label) => onSubmit(context, label), initialValue: label,
fromJsonT: fromJsonT,
submitButtonConfig: SubmitButtonConfig<T>(
icon: const Icon(Icons.save),
label: Text(S.of(context)!.saveChanges),
onSubmit: (label) => onSubmit(context, label),
),
additionalFields: additionalFields,
), ),
additionalFields: additionalFields,
), ),
); );
} }

View File

@@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
class SubmitButtonConfig<T extends Label> { class SubmitButtonConfig<T extends Label> {
@@ -36,6 +33,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
final List<Widget> additionalFields; final List<Widget> additionalFields;
final bool autofocusNameField; final bool autofocusNameField;
final GlobalKey<FormBuilderState>? formKey;
const LabelForm({ const LabelForm({
Key? key, Key? key,
@@ -44,6 +42,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
this.additionalFields = const [], this.additionalFields = const [],
required this.submitButtonConfig, required this.submitButtonConfig,
required this.autofocusNameField, required this.autofocusNameField,
this.formKey,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -51,7 +50,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
} }
class _LabelFormState<T extends Label> extends State<LabelForm<T>> { class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
final _formKey = GlobalKey<FormBuilderState>(); late final GlobalKey<FormBuilderState> _formKey;
late bool _enableMatchFormField; late bool _enableMatchFormField;
@@ -60,6 +59,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_formKey = widget.formKey ?? GlobalKey<FormBuilderState>();
var matchingAlgorithm = (widget.initialValue?.matchingAlgorithm ?? var matchingAlgorithm = (widget.initialValue?.matchingAlgorithm ??
MatchingAlgorithm.defaultValue); MatchingAlgorithm.defaultValue);
_enableMatchFormField = matchingAlgorithm != MatchingAlgorithm.auto && _enableMatchFormField = matchingAlgorithm != MatchingAlgorithm.auto &&

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -17,14 +16,9 @@ import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/verify_identity_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class HomeShellWidget extends StatelessWidget { class HomeShellWidget extends StatelessWidget {

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/theme.dart';
const _landingPage = 0; const _landingPage = 0;
const _documentsIndex = 1; const _documentsIndex = 1;
@@ -28,96 +30,105 @@ class ScaffoldWithNavigationBar extends StatefulWidget {
class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> { class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> {
@override @override
Widget build(BuildContext context) { void didChangeDependencies() {
final primaryColor = Theme.of(context).colorScheme.primary; super.didChangeDependencies();
}
return Scaffold( @override
drawer: const AppDrawer(), Widget build(BuildContext context) {
bottomNavigationBar: NavigationBar( final theme = Theme.of(context);
selectedIndex: widget.navigationShell.currentIndex, return AnnotatedRegion<SystemUiOverlayStyle>(
onDestinationSelected: (index) { value: buildOverlayStyle(theme),
widget.navigationShell.goBranch( child: Scaffold(
index, drawer: const AppDrawer(),
initialLocation: index == widget.navigationShell.currentIndex, bottomNavigationBar: NavigationBar(
); elevation: 3,
}, backgroundColor: Theme.of(context).colorScheme.surface,
destinations: [ selectedIndex: widget.navigationShell.currentIndex,
NavigationDestination( onDestinationSelected: (index) {
icon: const Icon(Icons.home_outlined), widget.navigationShell.goBranch(
selectedIcon: Icon( index,
Icons.home, initialLocation: index == widget.navigationShell.currentIndex,
color: primaryColor, );
), },
label: "Home", //TODO: INTL destinations: [
),
_toggleDestination(
NavigationDestination( NavigationDestination(
icon: const Icon(Icons.description_outlined), icon: const Icon(Icons.home_outlined),
selectedIcon: Icon( selectedIcon: Icon(
Icons.description, Icons.home,
color: primaryColor, color: theme.colorScheme.primary,
), ),
label: S.of(context)!.documents, label: S.of(context)!.home,
), ),
disableWhen: !widget.authenticatedUser.canViewDocuments, _toggleDestination(
), NavigationDestination(
_toggleDestination( icon: const Icon(Icons.description_outlined),
NavigationDestination( selectedIcon: Icon(
icon: const Icon(Icons.document_scanner_outlined), Icons.description,
selectedIcon: Icon( color: theme.colorScheme.primary,
Icons.document_scanner, ),
color: primaryColor, label: S.of(context)!.documents,
), ),
label: S.of(context)!.scanner, disableWhen: !widget.authenticatedUser.canViewDocuments,
), ),
disableWhen: !widget.authenticatedUser.canCreateDocuments, _toggleDestination(
), NavigationDestination(
_toggleDestination( icon: const Icon(Icons.document_scanner_outlined),
NavigationDestination( selectedIcon: Icon(
icon: const Icon(Icons.sell_outlined), Icons.document_scanner,
selectedIcon: Icon( color: theme.colorScheme.primary,
Icons.sell, ),
color: primaryColor, label: S.of(context)!.scanner,
), ),
label: S.of(context)!.labels, disableWhen: !widget.authenticatedUser.canCreateDocuments,
), ),
disableWhen: !widget.authenticatedUser.canViewAnyLabel, _toggleDestination(
), NavigationDestination(
_toggleDestination( icon: const Icon(Icons.sell_outlined),
NavigationDestination( selectedIcon: Icon(
icon: Builder( Icons.sell,
builder: (context) { color: theme.colorScheme.primary,
return BlocBuilder<InboxCubit, InboxState>( ),
builder: (context, state) { label: S.of(context)!.labels,
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: const Icon(Icons.inbox_outlined),
);
},
);
},
), ),
selectedIcon: BlocBuilder<InboxCubit, InboxState>( disableWhen: !widget.authenticatedUser.canViewAnyLabel,
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0 &&
widget.authenticatedUser.canViewInbox,
count: state.itemsInInboxCount,
child: Icon(
Icons.inbox,
color: primaryColor,
),
);
},
),
label: S.of(context)!.inbox,
), ),
disableWhen: !widget.authenticatedUser.canViewInbox, _toggleDestination(
), NavigationDestination(
], icon: Builder(
builder: (context) {
return BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: const Icon(Icons.inbox_outlined),
);
},
);
},
),
selectedIcon: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0 &&
widget.authenticatedUser.canViewInbox,
count: state.itemsInInboxCount,
child: Icon(
Icons.inbox,
color: theme.colorScheme.primary,
),
);
},
),
label: S.of(context)!.inbox,
),
disableWhen: !widget.authenticatedUser.canViewInbox,
),
],
),
body: widget.navigationShell,
), ),
body: widget.navigationShell,
); );
} }

View File

@@ -33,8 +33,40 @@ class _InboxPageState extends State<InboxPage>
@override @override
final pagingScrollController = ScrollController(); final pagingScrollController = ScrollController();
final _nestedScrollViewKey = GlobalKey<NestedScrollViewState>();
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>(); final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
final _scrollController = ScrollController(); final _scrollController = ScrollController();
bool _showExtendedFab = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_nestedScrollViewKey.currentState!.innerController
.addListener(_scrollExtentChangedListener);
});
}
@override
void dispose() {
_nestedScrollViewKey.currentState?.innerController
.removeListener(_scrollExtentChangedListener);
super.dispose();
}
void _scrollExtentChangedListener() {
const threshold = 400;
final offset =
_nestedScrollViewKey.currentState!.innerController.position.pixels;
if (offset < threshold && _showExtendedFab == false) {
setState(() {
_showExtendedFab = true;
});
} else if (offset >= threshold && _showExtendedFab == true) {
setState(() {
_showExtendedFab = false;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -48,9 +80,31 @@ class _InboxPageState extends State<InboxPage>
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return FloatingActionButton.extended( return FloatingActionButton.extended(
heroTag: "fab_inbox", extendedPadding: _showExtendedFab
label: Text(S.of(context)!.allSeen), ? null
icon: const Icon(Icons.done_all), : const EdgeInsets.symmetric(horizontal: 16),
heroTag: "inbox_page_fab",
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
child: child,
),
);
},
child: _showExtendedFab
? Row(
children: [
const Icon(Icons.done_all),
Text(S.of(context)!.allSeen),
],
)
: const Icon(Icons.done_all),
),
onPressed: state.hasLoaded && state.documents.isNotEmpty onPressed: state.hasLoaded && state.documents.isNotEmpty
? () => _onMarkAllAsSeen( ? () => _onMarkAllAsSeen(
state.documents, state.documents,
@@ -63,13 +117,9 @@ class _InboxPageState extends State<InboxPage>
body: SafeArea( body: SafeArea(
top: true, top: true,
child: NestedScrollView( child: NestedScrollView(
key: _nestedScrollViewKey,
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber( SliverSearchBar(titleText: S.of(context)!.inbox),
handle: searchBarHandle,
sliver: SliverSearchBar(
titleText: S.of(context)!.inbox,
),
)
], ],
body: BlocBuilder<InboxCubit, InboxState>( body: BlocBuilder<InboxCubit, InboxState>(
builder: (_, state) { builder: (_, state) {

View File

@@ -4,8 +4,8 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
part 'label_state.dart';
part 'label_cubit.freezed.dart'; part 'label_cubit.freezed.dart';
part 'label_state.dart';
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> { class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
@override @override
@@ -25,6 +25,15 @@ class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
); );
} }
Future<void> reload() {
return Future.wait([
labelRepository.findAllCorrespondents(),
labelRepository.findAllDocumentTypes(),
labelRepository.findAllTags(),
labelRepository.findAllStoragePaths(),
]);
}
@override @override
Future<void> close() { Future<void> close() {
labelRepository.removeListener(this); labelRepository.removeListener(this);

View File

@@ -30,6 +30,7 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapAbsorberHandle(); SliverOverlapAbsorberHandle();
late final TabController _tabController; late final TabController _tabController;
int _currentIndex = 0; int _currentIndex = 0;
int _calculateTabCount(UserModel user) => [ int _calculateTabCount(UserModel user) => [
@@ -48,6 +49,12 @@ class _LabelsPageState extends State<LabelsPage>
..addListener(() => setState(() => _currentIndex = _tabController.index)); ..addListener(() => setState(() => _currentIndex = _tabController.index));
} }
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder( return ValueListenableBuilder(
@@ -65,8 +72,17 @@ class _LabelsPageState extends State<LabelsPage>
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton.extended(
heroTag: "fab_labels_page", heroTag: "inbox_page_fab",
label: Text(
[
S.of(context)!.addCorrespondent,
S.of(context)!.addDocumentType,
S.of(context)!.addTag,
S.of(context)!.addStoragePath,
][_currentIndex],
),
icon: Icon(Icons.add),
onPressed: [ onPressed: [
if (user.canViewCorrespondents) if (user.canViewCorrespondents)
() => CreateLabelRoute(LabelType.correspondent) () => CreateLabelRoute(LabelType.correspondent)
@@ -80,7 +96,6 @@ class _LabelsPageState extends State<LabelsPage>
() => CreateLabelRoute(LabelType.storagePath) () => CreateLabelRoute(LabelType.storagePath)
.push(context), .push(context),
][_currentIndex], ][_currentIndex],
child: const Icon(Icons.add),
), ),
body: NestedScrollView( body: NestedScrollView(
floatHeaderSlivers: true, floatHeaderSlivers: true,

View File

@@ -42,7 +42,8 @@ class _LandingPageState extends State<LandingPage> {
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: Text( child: Text(
"Welcome, ${currentUser.fullName ?? currentUser.username}!", S.of(context)!.welcomeUser(
currentUser.fullName ?? currentUser.username),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@@ -81,13 +82,12 @@ class _LandingPageState extends State<LandingPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(S.of(context)!.noSavedViewOnHomepageHint)
"There are no saved views to show on your dashboard.", //TODO: INTL .padded(),
).padded(),
TextButton.icon( TextButton.icon(
onPressed: () {}, onPressed: () {},
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: Text("Add new view"), label: Text(S.of(context)!.newView),
) )
], ],
).paddedOnly(left: 16), ).paddedOnly(left: 16),
@@ -121,35 +121,23 @@ class _LandingPageState extends State<LandingPage> {
Widget _buildStatisticsCard(BuildContext context) { Widget _buildStatisticsCard(BuildContext context) {
final currentUser = context.read<LocalUserAccount>().paperlessUser; final currentUser = context.read<LocalUserAccount>().paperlessUser;
return FutureBuilder<PaperlessServerStatisticsModel>(
future: context.read<PaperlessServerStatsApi>().getServerStatistics(), return ExpansionCard(
builder: (context, snapshot) { initiallyExpanded: false,
if (!snapshot.hasData) { title: Text(
return Card( S.of(context)!.statistics,
margin: const EdgeInsets.all(16), style: Theme.of(context).textTheme.titleLarge,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, content: FutureBuilder<PaperlessServerStatisticsModel>(
children: [ future: context.read<PaperlessServerStatsApi>().getServerStatistics(),
Text( builder: (context, snapshot) {
"Statistics", //TODO: INTL if (!snapshot.hasData) {
style: Theme.of(context).textTheme.titleLarge, return const Center(
), child: CircularProgressIndicator(),
const Padding( ).paddedOnly(top: 8, bottom: 24);
padding: EdgeInsets.all(16.0), }
child: Center(child: CircularProgressIndicator()), final stats = snapshot.data!;
), return Column(
],
).padded(16),
);
}
final stats = snapshot.data!;
return ExpansionCard(
initiallyExpanded: false,
title: Text(
"Statistics", //TODO: INTL
style: Theme.of(context).textTheme.titleLarge,
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Card( Card(
@@ -157,7 +145,7 @@ class _LandingPageState extends State<LandingPage> {
child: ListTile( child: ListTile(
shape: Theme.of(context).cardTheme.shape, shape: Theme.of(context).cardTheme.shape,
titleTextStyle: Theme.of(context).textTheme.labelLarge, titleTextStyle: Theme.of(context).textTheme.labelLarge,
title: const Text("Documents in inbox:"), title: Text(S.of(context)!.documentsInInbox),
onTap: currentUser.canViewTags && currentUser.canViewDocuments onTap: currentUser.canViewTags && currentUser.canViewDocuments
? () => InboxRoute().go(context) ? () => InboxRoute().go(context)
: null, : null,
@@ -172,19 +160,13 @@ class _LandingPageState extends State<LandingPage> {
child: ListTile( child: ListTile(
shape: Theme.of(context).cardTheme.shape, shape: Theme.of(context).cardTheme.shape,
titleTextStyle: Theme.of(context).textTheme.labelLarge, titleTextStyle: Theme.of(context).textTheme.labelLarge,
title: const Text("Total documents:"), title: Text(S.of(context)!.totalDocuments),
onTap: () { onTap: () {
DocumentsRoute().go(context); DocumentsRoute().go(context);
}, },
trailing: Chip( trailing: Text(
padding: const EdgeInsets.symmetric( stats.documentsTotal.toString(),
horizontal: 4, style: Theme.of(context).textTheme.labelLarge,
vertical: 2,
),
labelPadding: const EdgeInsets.symmetric(horizontal: 4),
label: Text(
stats.documentsTotal.toString(),
),
), ),
), ),
), ),
@@ -193,16 +175,10 @@ class _LandingPageState extends State<LandingPage> {
child: ListTile( child: ListTile(
shape: Theme.of(context).cardTheme.shape, shape: Theme.of(context).cardTheme.shape,
titleTextStyle: Theme.of(context).textTheme.labelLarge, titleTextStyle: Theme.of(context).textTheme.labelLarge,
title: const Text("Total characters:"), title: Text(S.of(context)!.totalCharacters),
trailing: Chip( trailing: Text(
padding: const EdgeInsets.symmetric( stats.totalChars.toString(),
horizontal: 4, style: Theme.of(context).textTheme.labelLarge,
vertical: 2,
),
labelPadding: const EdgeInsets.symmetric(horizontal: 4),
label: Text(
stats.totalChars.toString(),
),
), ),
), ),
), ),
@@ -214,9 +190,9 @@ class _LandingPageState extends State<LandingPage> {
), ),
), ),
], ],
).padded(16), ).padded(16);
); },
}, ),
); );
} }
} }

View File

@@ -15,6 +15,7 @@ class ExpansionCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card( return Card(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
child: Theme( child: Theme(
@@ -29,8 +30,17 @@ class ExpansionCard extends StatelessWidget {
), ),
), ),
child: ExpansionTile( child: ExpansionTile(
backgroundColor: Theme.of(context).colorScheme.surfaceVariant, backgroundColor: ElevationOverlay.applySurfaceTint(
colorScheme.surface,
colorScheme.surfaceTint,
4,
),
initiallyExpanded: initiallyExpanded, initiallyExpanded: initiallyExpanded,
collapsedBackgroundColor: ElevationOverlay.applySurfaceTint(
colorScheme.surface,
colorScheme.surfaceTint,
4,
),
title: title, title: title,
children: [content], children: [content],
), ),

View File

@@ -16,6 +16,7 @@ import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
part 'authentication_cubit.freezed.dart'; part 'authentication_cubit.freezed.dart';
part 'authentication_state.dart'; part 'authentication_state.dart';
@@ -196,8 +197,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"restoreSessionState", "restoreSessionState",
"Biometric authentication required, waiting for user to authenticate...", "Biometric authentication required, waiting for user to authenticate...",
); );
final localAuthSuccess = await _localAuthService final authenticationMesage =
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL (await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag)))
.verifyYourIdentity;
final localAuthSuccess =
await _localAuthService.authenticateLocalUser(authenticationMesage);
if (!localAuthSuccess) { if (!localAuthSuccess) {
emit(const AuthenticationState.requriresLocalAuthentication()); emit(const AuthenticationState.requriresLocalAuthentication());
_debugPrintMessage( _debugPrintMessage(
@@ -233,7 +237,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
throw Exception( throw Exception(
"User should be authenticated but no authentication information was found.", "User should be authenticated but no authentication information was found.",
); //TODO: INTL );
} }
_debugPrintMessage( _debugPrintMessage(
"restoreSessionState", "restoreSessionState",

View File

@@ -130,7 +130,7 @@ class LocalNotificationService {
filePath: filePath, filePath: filePath,
).toJson(), ).toJson(),
), ),
); //TODO: INTL );
} }

View File

@@ -5,8 +5,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
part 'saved_view_state.dart';
part 'saved_view_cubit.freezed.dart'; part 'saved_view_cubit.freezed.dart';
part 'saved_view_state.dart';
class SavedViewCubit extends Cubit<SavedViewState> { class SavedViewCubit extends Cubit<SavedViewState> {
final SavedViewRepository _savedViewRepository; final SavedViewRepository _savedViewRepository;

View File

@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.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/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -28,53 +28,57 @@ class SavedViewPreview extends StatelessWidget {
return ExpansionCard( return ExpansionCard(
initiallyExpanded: expanded, initiallyExpanded: expanded,
title: Text(savedView.name), title: Text(savedView.name),
content: BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>( content: Column(
builder: (context, state) { crossAxisAlignment: CrossAxisAlignment.center,
return state.maybeWhen( children: [
loaded: (documents) { BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
return Column( builder: (context, state) {
children: [ return state.maybeWhen(
if (documents.isEmpty) loaded: (documents) {
Text("This view does not match any documents.").padded() if (documents.isEmpty) {
else return Text(S.of(context)!.noDocumentsFound).padded();
for (final document in documents) } else {
DocumentListItem( return Column(
document: document, children: [
isLabelClickable: false, for (final document in documents)
isSelected: false, DocumentListItem(
isSelectionActive: false, document: document,
onTap: (document) { isLabelClickable: false,
DocumentDetailsRoute($extra: document) isSelected: false,
.push(context); isSelectionActive: false,
}, onTap: (document) {
onSelected: null, DocumentDetailsRoute($extra: document)
), .push(context);
Row( },
mainAxisAlignment: MainAxisAlignment.end, onSelected: null,
children: [ ),
TextButton.icon( ],
icon: const Icon(Icons.open_in_new), );
label: Text("Show all"), //TODO: INTL }
onPressed: () { },
context.read<DocumentsCubit>().updateFilter( error: () => Text(S.of(context)!.couldNotLoadSavedViews),
filter: savedView.toDocumentFilter(), orElse: () => const Center(
); child: CircularProgressIndicator(),
DocumentsRoute().go(context); ).paddedOnly(top: 8, bottom: 24),
},
),
],
),
],
); );
}, },
error: () => ),
const Text("Could not load saved view."), //TODO: INTL Row(
orElse: () => const Padding( mainAxisAlignment: MainAxisAlignment.end,
padding: EdgeInsets.all(8.0), children: [
child: Center(child: CircularProgressIndicator()), TextButton.icon(
), icon: const Icon(Icons.open_in_new),
); label: Text(S.of(context)!.showAll),
}, onPressed: () {
context.read<DocumentsCubit>().updateFilter(
filter: savedView.toDocumentFilter(),
);
DocumentsRoute().go(context);
},
).paddedOnly(bottom: 8),
],
),
],
), ),
); );
}, },

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
@@ -8,6 +9,7 @@ import 'package:paperless_mobile/features/settings/model/color_scheme_option.dar
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/theme.dart';
class ColorSchemeOptionSetting extends StatelessWidget { class ColorSchemeOptionSetting extends StatelessWidget {
const ColorSchemeOptionSetting({super.key}); const ColorSchemeOptionSetting({super.key});
@@ -52,10 +54,10 @@ class ColorSchemeOptionSetting extends StatelessWidget {
initialValue: settings.preferredColorSchemeOption, initialValue: settings.preferredColorSchemeOption,
), ),
).then( ).then(
(value) { (value) async {
if (value != null) { if (value != null) {
settings.preferredColorSchemeOption = value; settings.preferredColorSchemeOption = value;
settings.save(); await settings.save();
} }
}, },
), ),

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/theme.dart';
class ThemeModeSetting extends StatelessWidget { class ThemeModeSetting extends StatelessWidget {
const ThemeModeSetting({super.key}); const ThemeModeSetting({super.key});
@@ -34,10 +36,10 @@ class ThemeModeSetting extends StatelessWidget {
) )
], ],
), ),
).then((value) { ).then((value) async {
if (value != null) { if (value != null) {
settings.preferredThemeMode = value; settings.preferredThemeMode = value;
settings.save(); await settings.save();
} }
}), }),
); );

View File

@@ -67,7 +67,7 @@
"@startTyping": {}, "@startTyping": {},
"doYouReallyWantToDeleteThisView": "Vols esborrar aquesta vista?", "doYouReallyWantToDeleteThisView": "Vols esborrar aquesta vista?",
"@doYouReallyWantToDeleteThisView": {}, "@doYouReallyWantToDeleteThisView": {},
"deleteView": "Esborra Vista ", "deleteView": "Esborra Vista {name}?",
"@deleteView": {}, "@deleteView": {},
"addedAt": "Afegit", "addedAt": "Afegit",
"@addedAt": {}, "@addedAt": {},
@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -67,7 +67,7 @@
"@startTyping": {}, "@startTyping": {},
"doYouReallyWantToDeleteThisView": "Opravdu chceš tento náhled smazat?", "doYouReallyWantToDeleteThisView": "Opravdu chceš tento náhled smazat?",
"@doYouReallyWantToDeleteThisView": {}, "@doYouReallyWantToDeleteThisView": {},
"deleteView": "Smazat náhled ", "deleteView": "Smazat náhled {name}?",
"@deleteView": {}, "@deleteView": {},
"addedAt": "Přidáno", "addedAt": "Přidáno",
"@addedAt": {}, "@addedAt": {},
@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -67,7 +67,7 @@
"@startTyping": {}, "@startTyping": {},
"doYouReallyWantToDeleteThisView": "Möchtest Du diese Ansicht wirklich löschen?", "doYouReallyWantToDeleteThisView": "Möchtest Du diese Ansicht wirklich löschen?",
"@doYouReallyWantToDeleteThisView": {}, "@doYouReallyWantToDeleteThisView": {},
"deleteView": "Lösche Ansicht ", "deleteView": "Ansicht {name} löschen?",
"@deleteView": {}, "@deleteView": {},
"addedAt": "Hinzugefügt am", "addedAt": "Hinzugefügt am",
"@addedAt": {}, "@addedAt": {},
@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "Keine Dokumente gefunden.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Korrespondent konnte nicht gelöscht werden, bitte versuche es erneut.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Dokumenttyp konnten nicht gelöscht werden, bitte versuche es erneut.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Tag konnte nicht gelöscht werden, bitte versuche es erneut.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Speicherpfad konnte nicht gelöscht werden, bitte versuchen Sie es erneut.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Korrespondent konnte nicht aktualisiert werden, bitte versuche es erneut.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Dokumenttyp konnte nicht aktualisiert werden, bitte versuche es erneut.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Tag konnte nicht aktualisiert werden, bitte versuche es erneut.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Serverinformationen konnten nicht geladen werden.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Serverstatistiken konnten nicht geladen werden.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "UI Einstellungen konnten nicht geladen werden.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Dateiaufgaben konnten nicht geladen werden.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "Der Nutzer konnte nicht gefunden werden.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Ansicht konnte nicht aktualisiert werden, bitte versuche es erneut.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Speicherpfad konnte nicht aktualisiert werden, bitte versuchen Sie es erneut.",
"savedViewSuccessfullyUpdated": "Ansicht erfolgreich aktualisiert.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Änderungen verwerfen?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "Die Filterbedingungen der aktiven Ansicht haben sich geändert. Durch Zurücksetzen des aktuellen Filters gehen diese Änderungen verloren. Möchtest du trotzdem fortfahren?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Vom aktuellen Filter erstellen",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Startseite",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Willkommen, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Konfiguriere eine Ansicht so, dass sie auf deiner Startseite angezeigt wird und sie wird hier erscheinen.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistiken",
"documentsInInbox": "Dokumente im Posteingang",
"totalDocuments": "Dokumente insgesamt",
"totalCharacters": "Zeichen insgesamt",
"showAll": "Alle anzeigen",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -67,7 +67,7 @@
"@startTyping": {}, "@startTyping": {},
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?", "doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
"@doYouReallyWantToDeleteThisView": {}, "@doYouReallyWantToDeleteThisView": {},
"deleteView": "Delete view ", "deleteView": "Delete view {name}?",
"@deleteView": {}, "@deleteView": {},
"addedAt": "Added at", "addedAt": "Added at",
"@addedAt": {}, "@addedAt": {},
@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -67,7 +67,7 @@
"@startTyping": {}, "@startTyping": {},
"doYouReallyWantToDeleteThisView": "Voulez-vous vraiment supprimer cette vue enregistrée ?", "doYouReallyWantToDeleteThisView": "Voulez-vous vraiment supprimer cette vue enregistrée ?",
"@doYouReallyWantToDeleteThisView": {}, "@doYouReallyWantToDeleteThisView": {},
"deleteView": "Supprimer la vue enregistrée ", "deleteView": "Supprimer la vue enregistrée {name}?",
"@deleteView": {}, "@deleteView": {},
"addedAt": "Date dajout", "addedAt": "Date dajout",
"@addedAt": {}, "@addedAt": {},
@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -876,5 +876,98 @@
"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": "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
},
"noDocumentsFound": "No documents found.",
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
"couldNotDeleteTag": "Could not delete tag, please try again.",
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
"couldNotUpdateCorrespondent": "Could not update correspondent, please try again.",
"@couldNotUpdateCorrespondent": {
"description": "Message shown when a correspondent could not be updated"
},
"couldNotUpdateDocumentType": "Could not update document type, please try again.",
"@couldNotUpdateDocumentType": {
"description": "Message shown when a document type could not be updated"
},
"couldNotUpdateTag": "Could not update tag, please try again.",
"@couldNotUpdateTag": {
"description": "Message shown when a tag could not be updated"
},
"couldNotLoadServerInformation": "Could not load server information.",
"@couldNotLoadServerInformation": {
"description": "Message shown when the server information could not be loaded"
},
"couldNotLoadStatistics": "Could not load server statistics.",
"@couldNotLoadStatistics": {
"description": "Message shown when the server statistics could not be loaded"
},
"couldNotLoadUISettings": "Could not load UI settings.",
"@couldNotLoadUISettings": {
"description": "Message shown when the UI settings could not be loaded"
},
"couldNotLoadTasks": "Could not load tasks.",
"@couldNotLoadTasks": {
"description": "Message shown when the tasks (e.g. document consumed) could not be loaded"
},
"userNotFound": "User could not be found.",
"@userNotFound": {
"description": "Message shown when the specified user (e.g. by id) could not be found"
},
"couldNotUpdateSavedView": "Could not update saved view, please try again.",
"@couldNotUpdateSavedView": {
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "Could not update storage path, please try again.",
"savedViewSuccessfullyUpdated": "Saved view successfully updated.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
"discardChanges": "Discard changes?",
"@discardChanges": {
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
},
"savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?",
"@savedViewChangedDialogContent": {
"description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view."
},
"createFromCurrentFilter": "Create from current filter",
"@createFromCurrentFilter": {
"description": "Tooltip of the \"New saved view\" button"
},
"home": "Home",
"@home": {
"description": "Label of the \"Home\" route"
},
"welcomeUser": "Welcome, {name}!",
"@welcomeUser": {
"description": "Top message shown on the home page"
},
"noSavedViewOnHomepageHint": "Configure a saved view to be displayed on your home page and it will show up here.",
"@noSavedViewOnHomepageHint": {
"description": "Message shown when there is no saved view to display on the home page."
},
"statistics": "Statistics",
"documentsInInbox": "Documents in inbox",
"totalDocuments": "Total documents",
"totalCharacters": "Total characters",
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
} }
} }

View File

@@ -6,6 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
@@ -34,7 +35,6 @@ 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/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.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/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/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart';
@@ -109,7 +109,6 @@ void main() async {
if (Platform.isIOS) { if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo; iosInfo = await DeviceInfoPlugin().iosInfo;
} }
final connectivity = Connectivity(); final connectivity = Connectivity();
final localAuthentication = LocalAuthentication(); final localAuthentication = LocalAuthentication();
final connectivityStatusService = final connectivityStatusService =
@@ -149,6 +148,7 @@ void main() async {
final authenticationCubit = final authenticationCubit =
AuthenticationCubit(localAuthService, apiFactory, sessionManager); AuthenticationCubit(localAuthService, apiFactory, sessionManager);
await authenticationCubit.restoreSessionState(); await authenticationCubit.restoreSessionState();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
@@ -228,11 +228,11 @@ class _GoRouterShellState extends State<GoRouterShell> {
$loginRoute, $loginRoute,
$verifyIdentityRoute, $verifyIdentityRoute,
$switchingAccountsRoute, $switchingAccountsRoute,
$settingsRoute,
ShellRoute( ShellRoute(
navigatorKey: rootNavigatorKey, navigatorKey: rootNavigatorKey,
builder: ProviderShellRoute(widget.apiFactory).build, builder: ProviderShellRoute(widget.apiFactory).build,
routes: [ routes: [
$settingsRoute,
$savedViewsRoute, $savedViewsRoute,
StatefulShellRoute( StatefulShellRoute(
navigatorContainerBuilder: (context, navigationShell, children) { navigatorContainerBuilder: (context, navigationShell, children) {

View File

@@ -6,3 +6,4 @@ final documentsNavigatorKey = GlobalKey<NavigatorState>();
final scannerNavigatorKey = GlobalKey<NavigatorState>(); final scannerNavigatorKey = GlobalKey<NavigatorState>();
final labelsNavigatorKey = GlobalKey<NavigatorState>(); final labelsNavigatorKey = GlobalKey<NavigatorState>();
final inboxNavigatorKey = GlobalKey<NavigatorState>(); final inboxNavigatorKey = GlobalKey<NavigatorState>();
final settingsNavigatorKey = GlobalKey<NavigatorState>();

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -14,6 +15,7 @@ import 'package:paperless_mobile/features/documents/view/pages/documents_page.da
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart'; import 'package:paperless_mobile/routes/routes.dart';
import 'package:paperless_mobile/theme.dart';
part 'documents_route.g.dart'; part 'documents_route.g.dart';
@@ -92,14 +94,21 @@ class EditDocumentRoute extends GoRouteData {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return BlocProvider( final theme = Theme.of(context);
create: (context) => DocumentEditCubit( return AnnotatedRegion<SystemUiOverlayStyle>(
context.read(), value: buildOverlayStyle(
context.read(), theme,
context.read(), systemNavigationBarColor: theme.colorScheme.background,
document: $extra, ),
)..loadFieldSuggestions(), child: BlocProvider(
child: const DocumentEditPage(), create: (context) => DocumentEditCubit(
context.read(),
context.read(),
context.read(),
document: $extra,
)..loadFieldSuggestions(),
child: const DocumentEditPage(),
),
); );
} }
} }

View File

@@ -15,12 +15,6 @@ class LandingBranch extends StatefulShellBranchData {
@TypedGoRoute<LandingRoute>( @TypedGoRoute<LandingRoute>(
path: "/landing", path: "/landing",
name: R.landing, name: R.landing,
routes: [
TypedGoRoute<SavedViewRoute>(
path: "saved-view",
name: R.savedView,
),
],
) )
class LandingRoute extends GoRouteData { class LandingRoute extends GoRouteData {
const LandingRoute(); const LandingRoute();
@@ -29,10 +23,3 @@ class LandingRoute extends GoRouteData {
return const LandingPage(); return const LandingPage();
} }
} }
class SavedViewRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return Placeholder();
}
}

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart'; import 'package:paperless_mobile/routes/routes.dart';
import 'package:paperless_mobile/theme.dart';
part 'settings_route.g.dart'; part 'settings_route.g.dart';
@@ -10,8 +13,16 @@ part 'settings_route.g.dart';
name: R.settings, name: R.settings,
) )
class SettingsRoute extends GoRouteData { class SettingsRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return const SettingsPage(); return AnnotatedRegion<SystemUiOverlayStyle>(
value: buildOverlayStyle(
Theme.of(context),
systemNavigationBarColor: Theme.of(context).colorScheme.background,
),
child: const SettingsPage(),
);
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
const _classicThemeColorSeed = Colors.lightGreen; const _classicThemeColorSeed = Colors.lightGreen;
@@ -46,6 +47,12 @@ ThemeData buildTheme({
colorScheme: colorScheme.harmonized(), colorScheme: colorScheme.harmonized(),
useMaterial3: true, useMaterial3: true,
).copyWith( ).copyWith(
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: colorScheme.surface,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: colorScheme.surface,
),
cardTheme: _defaultCardTheme, cardTheme: _defaultCardTheme,
inputDecorationTheme: _defaultInputDecorationTheme, inputDecorationTheme: _defaultInputDecorationTheme,
listTileTheme: _defaultListTileTheme, listTileTheme: _defaultListTileTheme,
@@ -60,3 +67,29 @@ ThemeData buildTheme({
), ),
); );
} }
SystemUiOverlayStyle buildOverlayStyle(
ThemeData theme, {
Color? systemNavigationBarColor,
}) {
final color = systemNavigationBarColor ??
ElevationOverlay.applySurfaceTint(
theme.colorScheme.surface,
theme.colorScheme.surfaceTint,
3,
);
return switch (theme.brightness) {
Brightness.light => SystemUiOverlayStyle.dark.copyWith(
systemNavigationBarColor: color,
systemNavigationBarDividerColor: color,
// statusBarColor: theme.colorScheme.background,
// systemNavigationBarDividerColor: theme.colorScheme.surface,
),
Brightness.dark => SystemUiOverlayStyle.light.copyWith(
systemNavigationBarColor: color,
systemNavigationBarDividerColor: color,
// statusBarColor: theme.colorScheme.background,
// systemNavigationBarDividerColor: theme.colorScheme.surface,
),
};
}

View File

@@ -16,7 +16,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
@override @override
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async { Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final result = await getCollection( final result = await getCollection(
"/api/saved_views/", "/api/saved_views/?page_size=100000",
SavedView.fromJson, SavedView.fromJson,
ErrorCode.loadSavedViewsError, ErrorCode.loadSavedViewsError,
client: _client, client: _client,

View File

@@ -345,6 +345,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.8" version: "0.7.8"
defer_pointer:
dependency: "direct main"
description:
name: defer_pointer
sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02
url: "https://pub.dev"
source: hosted
version: "0.0.2"
dependency_validator: dependency_validator:
dependency: "direct dev" dependency: "direct dev"
description: description:

View File

@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 2.3.11+46 version: 2.3.12+47
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@@ -93,6 +93,7 @@ dependencies:
go_router: ^10.0.0 go_router: ^10.0.0
fl_chart: ^0.63.0 fl_chart: ^0.63.0
palette_generator: ^0.3.3+2 palette_generator: ^0.3.3+2
defer_pointer: ^0.0.2
dependency_overrides: dependency_overrides:
intl: ^0.18.1 intl: ^0.18.1