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.
///
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 box = await Hive.openBox<T>(
name,
@@ -22,7 +24,11 @@ Future<R?> withEncryptedBox<T, R>(
}
Future<Uint8List> _getEncryptedBoxKey() async {
const secureStorage = FlutterSecureStorage();
const secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
if (!await secureStorage.containsKey(key: 'key')) {
final key = Hive.generateSecureKey();

View File

@@ -1,8 +1,7 @@
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_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_settings.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/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/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/view/saved_view_details_page.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);
void addListener(
Object source, {
Object subscriber, {
required void Function(T) onChanged,
}) {
onChanged(state);
_subscribers.putIfAbsent(source, () {
_subscribers.putIfAbsent(subscriber, () {
return stream.listen((event) => onChanged(event));
});
}
void removeListener(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
_subscribers
..[source]?.cancel()
..remove(source);
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
for (final subscriber in _subscribers.values) {
subscriber.cancel();
}
return super.close();
}
}

View File

@@ -54,25 +54,26 @@ String translateError(BuildContext context, ErrorCode code) {
ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions,
ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks,
ErrorCode.correspondentDeleteFailed =>
"Could not delete correspondent, please try again.", //TODO: INTL
S.of(context)!.couldNotDeleteCorrespondent,
ErrorCode.documentTypeDeleteFailed =>
"Could not delete document type, please try again.",
ErrorCode.tagDeleteFailed => "Could not delete tag, please try again.",
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.",
S.of(context)!.couldNotDeleteDocumentType,
ErrorCode.tagDeleteFailed => S.of(context)!.couldNotDeleteTag,
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 =>
"Could not update storage path, please try again.",
S.of(context)!.couldNotUpdateStoragePath,
ErrorCode.serverInformationLoadFailed =>
"Could not load server information.",
ErrorCode.serverStatisticsLoadFailed => "Could not load server statistics.",
ErrorCode.uiSettingsLoadFailed => "Could not load UI settings",
ErrorCode.loadTasksError => "Could not load tasks.",
ErrorCode.userNotFound => "User could not be found.",
ErrorCode.updateSavedViewError => "Could not update saved view.",
S.of(context)!.couldNotLoadServerInformation,
ErrorCode.serverStatisticsLoadFailed =>
S.of(context)!.couldNotLoadStatistics,
ErrorCode.uiSettingsLoadFailed => S.of(context)!.couldNotLoadUISettings,
ErrorCode.loadTasksError => S.of(context)!.couldNotLoadTasks,
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_permissions_widget.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/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/view/similar_documents_view.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_typeahead/flutter_typeahead.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/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/extensions/flutter_extensions.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/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
class DocumentEditPage extends StatefulWidget {
@@ -46,253 +45,257 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override
Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) {
final filteredSuggestions = state.suggestions?.documentDifference(
context.read<DocumentEditCubit>().state.document);
return DefaultTabController(
length: 2,
child: Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
heroTag: "fab_document_edit",
onPressed: () => _onSubmit(state.document),
icon: const Icon(Icons.save),
label: Text(S.of(context)!.saveChanges),
),
appBar: AppBar(
title: Text(S.of(context)!.editDocument),
bottom: TabBar(
tabs: [
Tab(
text: S.of(context)!.overview,
),
Tab(
text: S.of(context)!.content,
)
],
return PopWithUnsavedChanges(
hasChangesPredicate: () => _formKey.currentState?.isDirty ?? false,
child: BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) {
final filteredSuggestions = state.suggestions?.documentDifference(
context.read<DocumentEditCubit>().state.document);
return DefaultTabController(
length: 2,
child: Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
heroTag: "fab_document_edit",
onPressed: () => _onSubmit(state.document),
icon: const Icon(Icons.save),
label: Text(S.of(context)!.saveChanges),
),
),
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),
],
),
),
appBar: AppBar(
title: Text(S.of(context)!.editDocument),
bottom: TabBar(
tabs: [
Tab(text: S.of(context)!.overview),
Tab(text: S.of(context)!.content)
],
),
),
)),
);
},
extendBody: true,
body: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 8,
left: 8,
right: 8,
),
child: FormBuilder(
key: _formKey,
child: TabBarView(
children: [
ListView(
children: [
_buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(
state.document.created,
filteredSuggestions,
).padded(),
// Correspondent form field
if (currentUser.canViewCorrespondents)
Column(
children: [
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
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/database/tables/global_settings.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_service.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>>(
builder: (context, state) {
return SafeArea(
top: true,
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton(

View File

@@ -24,8 +24,10 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
this.api,
this.notifier,
this._userAppState,
) : super(DocumentSearchState(
searchHistory: _userAppState.documentSearchHistory)) {
) : super(
DocumentSearchState(
searchHistory: _userAppState.documentSearchHistory),
) {
notifier.addListener(
this,
onDeleted: remove,
@@ -34,22 +36,25 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
}
Future<void> search(String query) async {
emit(state.copyWith(
isLoading: true,
suggestions: [],
view: SearchView.results,
));
final normalizedQuery = query.trim();
emit(
state.copyWith(
isLoading: true,
suggestions: [],
view: SearchView.results,
),
);
final searchFilter = DocumentFilter(
query: TextQuery.extended(query),
query: TextQuery.extended(normalizedQuery),
);
await updateFilter(filter: searchFilter);
emit(
state.copyWith(
searchHistory: [
query,
normalizedQuery,
...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> {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 8),
child: OpenContainer(
transitionDuration: const Duration(milliseconds: 200),
transitionType: ContainerTransitionType.fadeThrough,
closedElevation: 1,
middleColor: Theme.of(context).colorScheme.surfaceVariant,
openColor: Theme.of(context).colorScheme.background,
closedColor: Theme.of(context).colorScheme.surfaceVariant,
closedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
closedBuilder: (_, action) {
return InkWell(
onTap: action,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 720,
minWidth: 360,
maxHeight: 56,
minHeight: 48,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.menu),
onPressed: Scaffold.of(context).openDrawer,
return OpenContainer(
transitionDuration: const Duration(milliseconds: 200),
transitionType: ContainerTransitionType.fadeThrough,
closedElevation: 1,
middleColor: Theme.of(context).colorScheme.surfaceVariant,
openColor: Theme.of(context).colorScheme.background,
closedColor: Theme.of(context).colorScheme.surfaceVariant,
closedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
closedBuilder: (_, action) {
return InkWell(
onTap: action,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 720,
minWidth: 360,
maxHeight: 56,
minHeight: 48,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
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(
create: (_) => DocumentSearchCubit(
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(context.read<LocalUserAccount>().id)!,
),
child: const DocumentSearchPage(),
);
},
),
),
);
},
openBuilder: (_, action) {
return Provider(
create: (_) => DocumentSearchCubit(
context.read(),
context.read(),
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(context.read<LocalUserAccount>().id)!,
),
child: const DocumentSearchPage(),
);
},
);
}

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart';
import 'package:defer_pointer/defer_pointer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
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/document_search/view/sliver_search_bar.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/view_type_selection_widget.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/view/saved_view_list.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/helpers/message_helpers.dart';
@@ -43,45 +43,58 @@ class DocumentsPage extends StatefulWidget {
State<DocumentsPage> createState() => _DocumentsPageState();
}
class _DocumentsPageState extends State<DocumentsPage>
with SingleTickerProviderStateMixin {
class _DocumentsPageState extends State<DocumentsPage> {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle savedViewsHandle =
SliverOverlapAbsorberHandle();
late final TabController _tabController;
int _currentTab = 0;
final _nestedScrollViewKey = GlobalKey<NestedScrollViewState>();
final _savedViewsExpansionController = ExpansionTileController();
bool _showExtendedFab = true;
@override
void initState() {
super.initState();
final showSavedViews =
context.read<LocalUserAccount>().paperlessUser.canViewSavedViews;
_tabController = TabController(
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);
WidgetsBinding.instance.addPostFrameCallback((_) {
_nestedScrollViewKey.currentState!.innerController
.addListener(_scrollExtentChangedListener);
});
}
void _tabChangesListener() {
setState(() => _currentTab = _tabController.index);
Future<void> _reloadData() async {
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
void dispose() {
_tabController.dispose();
_nestedScrollViewKey.currentState?.innerController
.removeListener(_scrollExtentChangedListener);
super.dispose();
}
@@ -109,11 +122,7 @@ class _DocumentsPageState extends State<DocumentsPage>
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) {
try {
context.read<DocumentsCubit>().reload();
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
_reloadData();
},
builder: (context, connectivityState) {
return SafeArea(
@@ -122,59 +131,104 @@ class _DocumentsPageState extends State<DocumentsPage>
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
final show = state.selection.isEmpty;
final canReset = state.filter.appliedFiltersCount > 0;
return AnimatedScale(
scale: show ? 1 : 0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeIn,
child: Column(
if (show) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (canReset)
Padding(
padding: const EdgeInsets.all(8.0),
child: FloatingActionButton.small(
heroTag: "fab_documents_page_reset_filter",
backgroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
onPressed: () {
_onResetFilter();
},
child: Icon(
Icons.refresh,
color: Theme.of(context)
.colorScheme
.primaryContainer,
DeferredPointerHandler(
child: Stack(
clipBehavior: Clip.none,
children: [
FloatingActionButton.extended(
extendedPadding: _showExtendedFab
? null
: const EdgeInsets.symmetric(
horizontal: 16),
heroTag: "fab_documents_page_filter",
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
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,
@@ -190,94 +244,41 @@ class _DocumentsPageState extends State<DocumentsPage>
}
return true;
},
child: Stack(
children: [
NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isEmpty) {
return SliverSearchBar(
floating: true,
titleText: S.of(context)!.documents,
);
} else {
return DocumentSelectionSliverAppBar(
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;
child: NestedScrollView(
key: _nestedScrollViewKey,
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isEmpty) {
return SliverSearchBar(
floating: true,
titleText: S.of(context)!.documents,
);
} else {
return DocumentSelectionSliverAppBar(
state: state,
);
}
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(
ConnectivityState connectivityState,
BuildContext context,
@@ -376,12 +301,11 @@ class _DocumentsPageState extends State<DocumentsPage>
_savedViewsExpansionController.collapse();
}
final currState = context.read<DocumentsCubit>().state;
final max = notification.metrics.maxScrollExtent;
final currentState = context.read<DocumentsCubit>().state;
if (max == 0 ||
_currentTab != 0 ||
currState.isLoading ||
currState.isLastPageLoaded) {
currentState.isLoading ||
currentState.isLastPageLoaded) {
return false;
}
@@ -402,7 +326,7 @@ class _DocumentsPageState extends State<DocumentsPage>
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight + 2,
onRefresh: _onReloadDocuments,
onRefresh: _reloadData,
notificationPredicate: (_) => connectivityState.isConnected,
child: CustomScrollView(
key: const PageStorageKey<String>("documents"),
@@ -428,8 +352,8 @@ class _DocumentsPageState extends State<DocumentsPage>
},
onUpdateView: (view) async {
await context.read<SavedViewCubit>().update(view);
showSnackBar(context,
"Saved view successfully updated."); //TODO: INTL
showSnackBar(
context, S.of(context)!.savedViewSuccessfullyUpdated);
},
onDeleteView: (view) async {
HapticFeedback.mediumImpact();
@@ -496,7 +420,7 @@ class _DocumentsPageState extends State<DocumentsPage>
return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return Container(
padding: EdgeInsets.all(4),
padding: const EdgeInsets.all(4),
color: Theme.of(context).colorScheme.background,
child: Row(
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 {
final draggableSheetController = DraggableScrollableController();
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
@@ -717,66 +629,46 @@ class _DocumentsPageState extends State<DocumentsPage>
}
}
Future<void> _onReloadDocuments() async {
try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await Future.wait([
context.read<DocumentsCubit>().reload(),
context.read<SavedViewCubit>().reload(),
]);
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
///
/// Resets the current filter and scrolls all the way to the top of the view.
/// If a saved view is currently selected and the filter has changed,
/// the user will be shown a dialog informing them about the changes.
/// The user can then decide whether to abort the reset or to continue and discard the changes.
Future<void> _onResetFilter() async {
final cubit = context.read<DocumentsCubit>();
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) {
if (cubit.state.filter.selectedView != null) {
return state.savedViews[cubit.state.filter.selectedView!];
}
return null;
},
orElse: () => null,
);
final viewHasChanged = activeView != null &&
activeView.toDocumentFilter() != cubit.state.filter;
if (viewHasChanged) {
final discardChanges = await showDialog(
context: context,
builder: (context) => const SavedViewChangedDialog(),
);
if (discardChanges == true) {
final discardChanges = await showDialog<bool>(
context: context,
builder: (context) => const SavedViewChangedDialog(),
) ??
false;
if (discardChanges) {
cubit.resetFilter();
// Reset
} else if (discardChanges == false) {
_updateCurrentSavedView();
toTop();
}
} else {
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/services.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/features/paged_document_view/cubit/paged_documents_state.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 {
final DocumentPagingState state;
final VoidCallback? onReset;
const DocumentsEmptyState({
Key? key,
required this.state,
@@ -17,18 +18,24 @@ class DocumentsEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: EmptyState(
title: S.of(context)!.oops,
subtitle: S.of(context)!.thereSeemsToBeNothingHere,
bottomChild: state.filter != DocumentFilter.initial && onReset != null
? TextButton(
onPressed: onReset,
child: Text(
S.of(context)!.resetFilter,
),
).padded()
: null,
),
child: Column(
children: [
Text(
S.of(context)!.noDocumentsFound,
style: Theme.of(context).textTheme.titleSmall,
),
if (state.filter != DocumentFilter.initial && onReset != null)
TextButton(
onPressed: () {
HapticFeedback.mediumImpact();
onReset!();
},
child: Text(
S.of(context)!.resetFilter,
),
).padded(),
],
).padded(24),
);
}
}

View File

@@ -9,19 +9,11 @@ class SavedViewChangedDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Discard changes?"), //TODO: INTL
content: Text(
"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
),
title: Text(S.of(context)!.discardChanges),
content: Text(S.of(context)!.savedViewChangedDialogContent),
actionsOverflowButtonSpacing: 8,
actions: [
const DialogCancelButton(),
// TextButton(
// child: Text(S.of(context)!.saveChanges),
// onPressed: () {
// Navigator.pop(context, false);
// },
// ),
DialogConfirmButton(
label: S.of(context)!.resetFilter,
style: DialogConfirmButtonStyle.danger,

View File

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

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_chip.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
@@ -46,99 +46,185 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
@override
Widget build(BuildContext context) {
return PageStorage(
bucket: PageStorageBucket(),
child: ExpansionTile(
controller: widget.controller,
tilePadding: const EdgeInsets.only(left: 8),
trailing: RotationTransition(
turns: _animation,
child: const Icon(Icons.expand_more),
).paddedOnly(right: 8),
onExpansionChanged: (isExpanded) {
if (isExpanded) {
_animationController.forward();
} else {
_animationController.reverse().then((value) => setState(() {}));
}
},
title: Text(
S.of(context)!.views,
style: Theme.of(context).textTheme.labelLarge,
),
leading: Icon(
Icons.saved_search,
color: Theme.of(context).colorScheme.primary,
).padded(),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
return state.map(
initial: (_) => const Placeholder(),
loading: (_) => const Placeholder(),
loaded: (value) {
if (value.savedViews.isEmpty) {
return Text(S.of(context)!.noItemsFound)
.paddedOnly(left: 16);
}
return Container(
margin: EdgeInsets.only(top: 16),
height: kMinInteractiveDimension,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) => true,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: [
const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
SliverList.separated(
itemBuilder: (context, index) {
final view =
value.savedViews.values.elementAt(index);
final isSelected =
(widget.filter.selectedView ?? -1) == view.id;
return SavedViewChip(
view: view,
onViewSelected: widget.onViewSelected,
selected: isSelected,
hasChanged: isSelected &&
view.toDocumentFilter() != widget.filter,
onUpdateView: widget.onUpdateView,
onDeleteView: widget.onDeleteView,
);
},
separatorBuilder: (context, index) =>
const SizedBox(width: 8),
itemCount: value.savedViews.length,
),
const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
],
return BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
final selectedView = state.mapOrNull(
loaded: (value) {
if (widget.filter.selectedView != null) {
return value.savedViews[widget.filter.selectedView!];
}
},
);
final selectedViewHasChanged = selectedView != null &&
selectedView.toDocumentFilter() != widget.filter;
return PageStorage(
bucket: PageStorageBucket(),
child: ExpansionTile(
controller: widget.controller,
tilePadding: const EdgeInsets.only(left: 8),
trailing: RotationTransition(
turns: _animation,
child: const Icon(Icons.expand_more),
).paddedOnly(right: 8),
onExpansionChanged: (isExpanded) {
if (isExpanded) {
_animationController.forward();
} else {
_animationController.reverse().then((value) => setState(() {}));
}
},
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context)!.views,
style: Theme.of(context).textTheme.labelLarge,
),
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(),
);
},
),
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),
separatorBuilder: (context, index) => const SizedBox(width: 8),
),
).padded(4),
const SliverToBoxAdapter(
child: SizedBox(width: 12),
),
],
),
],
),
),
);
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/features/documents/cubit/documents_cubit.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_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/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_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/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
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<void> Function(BuildContext context, T label) onDelete;
final bool canDelete;
final _formKey = GlobalKey<FormBuilderState>();
const EditLabelForm({
EditLabelForm({
super.key,
required this.label,
required this.fromJsonT,
@@ -69,26 +71,32 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.edit),
actions: [
IconButton(
onPressed: canDelete ? () => _onDelete(context) : null,
icon: const Icon(Icons.delete),
),
],
),
body: LabelForm<T>(
autofocusNameField: false,
initialValue: label,
fromJsonT: fromJsonT,
submitButtonConfig: SubmitButtonConfig<T>(
icon: const Icon(Icons.save),
label: Text(S.of(context)!.saveChanges),
onSubmit: (label) => onSubmit(context, label),
return PopWithUnsavedChanges(
hasChangesPredicate: () {
return _formKey.currentState?.isDirty ?? false;
},
child: Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.edit),
actions: [
IconButton(
onPressed: canDelete ? () => _onDelete(context) : null,
icon: const Icon(Icons.delete),
),
],
),
body: LabelForm<T>(
formKey: _formKey,
autofocusNameField: false,
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_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.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/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/helpers/message_helpers.dart';
class SubmitButtonConfig<T extends Label> {
@@ -36,6 +33,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
final List<Widget> additionalFields;
final bool autofocusNameField;
final GlobalKey<FormBuilderState>? formKey;
const LabelForm({
Key? key,
@@ -44,6 +42,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
this.additionalFields = const [],
required this.submitButtonConfig,
required this.autofocusNameField,
this.formKey,
}) : super(key: key);
@override
@@ -51,7 +50,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
}
class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
final _formKey = GlobalKey<FormBuilderState>();
late final GlobalKey<FormBuilderState> _formKey;
late bool _enableMatchFormField;
@@ -60,6 +59,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
@override
void initState() {
super.initState();
_formKey = widget.formKey ?? GlobalKey<FormBuilderState>();
var matchingAlgorithm = (widget.initialValue?.matchingAlgorithm ??
MatchingAlgorithm.defaultValue);
_enableMatchFormField = matchingAlgorithm != MatchingAlgorithm.auto &&

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.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/inbox/cubit/inbox_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/settings/view/widgets/global_settings_builder.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';
class HomeShellWidget extends StatelessWidget {

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/theme.dart';
const _landingPage = 0;
const _documentsIndex = 1;
@@ -28,96 +30,105 @@ class ScaffoldWithNavigationBar extends StatefulWidget {
class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> {
@override
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).colorScheme.primary;
void didChangeDependencies() {
super.didChangeDependencies();
}
return Scaffold(
drawer: const AppDrawer(),
bottomNavigationBar: NavigationBar(
selectedIndex: widget.navigationShell.currentIndex,
onDestinationSelected: (index) {
widget.navigationShell.goBranch(
index,
initialLocation: index == widget.navigationShell.currentIndex,
);
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: Icon(
Icons.home,
color: primaryColor,
),
label: "Home", //TODO: INTL
),
_toggleDestination(
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: buildOverlayStyle(theme),
child: Scaffold(
drawer: const AppDrawer(),
bottomNavigationBar: NavigationBar(
elevation: 3,
backgroundColor: Theme.of(context).colorScheme.surface,
selectedIndex: widget.navigationShell.currentIndex,
onDestinationSelected: (index) {
widget.navigationShell.goBranch(
index,
initialLocation: index == widget.navigationShell.currentIndex,
);
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.description_outlined),
icon: const Icon(Icons.home_outlined),
selectedIcon: Icon(
Icons.description,
color: primaryColor,
Icons.home,
color: theme.colorScheme.primary,
),
label: S.of(context)!.documents,
label: S.of(context)!.home,
),
disableWhen: !widget.authenticatedUser.canViewDocuments,
),
_toggleDestination(
NavigationDestination(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: primaryColor,
_toggleDestination(
NavigationDestination(
icon: const Icon(Icons.description_outlined),
selectedIcon: Icon(
Icons.description,
color: theme.colorScheme.primary,
),
label: S.of(context)!.documents,
),
label: S.of(context)!.scanner,
disableWhen: !widget.authenticatedUser.canViewDocuments,
),
disableWhen: !widget.authenticatedUser.canCreateDocuments,
),
_toggleDestination(
NavigationDestination(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
Icons.sell,
color: primaryColor,
_toggleDestination(
NavigationDestination(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
Icons.document_scanner,
color: theme.colorScheme.primary,
),
label: S.of(context)!.scanner,
),
label: S.of(context)!.labels,
disableWhen: !widget.authenticatedUser.canCreateDocuments,
),
disableWhen: !widget.authenticatedUser.canViewAnyLabel,
),
_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),
);
},
);
},
_toggleDestination(
NavigationDestination(
icon: const Icon(Icons.sell_outlined),
selectedIcon: Icon(
Icons.sell,
color: theme.colorScheme.primary,
),
label: S.of(context)!.labels,
),
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: primaryColor,
),
);
},
),
label: S.of(context)!.inbox,
disableWhen: !widget.authenticatedUser.canViewAnyLabel,
),
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
final pagingScrollController = ScrollController();
final _nestedScrollViewKey = GlobalKey<NestedScrollViewState>();
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
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
Widget build(BuildContext context) {
@@ -48,9 +80,31 @@ class _InboxPageState extends State<InboxPage>
return const SizedBox.shrink();
}
return FloatingActionButton.extended(
heroTag: "fab_inbox",
label: Text(S.of(context)!.allSeen),
icon: const Icon(Icons.done_all),
extendedPadding: _showExtendedFab
? null
: 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
? () => _onMarkAllAsSeen(
state.documents,
@@ -63,13 +117,9 @@ class _InboxPageState extends State<InboxPage>
body: SafeArea(
top: true,
child: NestedScrollView(
key: _nestedScrollViewKey,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: SliverSearchBar(
titleText: S.of(context)!.inbox,
),
)
SliverSearchBar(titleText: S.of(context)!.inbox),
],
body: BlocBuilder<InboxCubit, InboxState>(
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/features/labels/cubit/label_cubit_mixin.dart';
part 'label_state.dart';
part 'label_cubit.freezed.dart';
part 'label_state.dart';
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
@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
Future<void> close() {
labelRepository.removeListener(this);

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ class ExpansionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
margin: const EdgeInsets.all(16),
child: Theme(
@@ -29,8 +30,17 @@ class ExpansionCard extends StatelessWidget {
),
),
child: ExpansionTile(
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
backgroundColor: ElevationOverlay.applySurfaceTint(
colorScheme.surface,
colorScheme.surfaceTint,
4,
),
initiallyExpanded: initiallyExpanded,
collapsedBackgroundColor: ElevationOverlay.applySurfaceTint(
colorScheme.surface,
colorScheme.surfaceTint,
4,
),
title: title,
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/login_form_credentials.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_state.dart';
@@ -196,8 +197,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"restoreSessionState",
"Biometric authentication required, waiting for user to authenticate...",
);
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
final authenticationMesage =
(await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag)))
.verifyYourIdentity;
final localAuthSuccess =
await _localAuthService.authenticateLocalUser(authenticationMesage);
if (!localAuthSuccess) {
emit(const AuthenticationState.requriresLocalAuthentication());
_debugPrintMessage(
@@ -233,7 +237,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
);
throw Exception(
"User should be authenticated but no authentication information was found.",
); //TODO: INTL
);
}
_debugPrintMessage(
"restoreSessionState",

View File

@@ -130,7 +130,7 @@ class LocalNotificationService {
filePath: filePath,
).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_mobile/core/repository/saved_view_repository.dart';
part 'saved_view_state.dart';
part 'saved_view_cubit.freezed.dart';
part 'saved_view_state.dart';
class SavedViewCubit extends Cubit<SavedViewState> {
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:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

@@ -28,53 +28,57 @@ class SavedViewPreview extends StatelessWidget {
return ExpansionCard(
initiallyExpanded: expanded,
title: Text(savedView.name),
content: BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
builder: (context, state) {
return state.maybeWhen(
loaded: (documents) {
return Column(
children: [
if (documents.isEmpty)
Text("This view does not match any documents.").padded()
else
for (final document in documents)
DocumentListItem(
document: document,
isLabelClickable: false,
isSelected: false,
isSelectionActive: false,
onTap: (document) {
DocumentDetailsRoute($extra: document)
.push(context);
},
onSelected: null,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
icon: const Icon(Icons.open_in_new),
label: Text("Show all"), //TODO: INTL
onPressed: () {
context.read<DocumentsCubit>().updateFilter(
filter: savedView.toDocumentFilter(),
);
DocumentsRoute().go(context);
},
),
],
),
],
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
builder: (context, state) {
return state.maybeWhen(
loaded: (documents) {
if (documents.isEmpty) {
return Text(S.of(context)!.noDocumentsFound).padded();
} else {
return Column(
children: [
for (final document in documents)
DocumentListItem(
document: document,
isLabelClickable: false,
isSelected: false,
isSelectionActive: false,
onTap: (document) {
DocumentDetailsRoute($extra: document)
.push(context);
},
onSelected: null,
),
],
);
}
},
error: () => Text(S.of(context)!.couldNotLoadSavedViews),
orElse: () => const Center(
child: CircularProgressIndicator(),
).paddedOnly(top: 8, bottom: 24),
);
},
error: () =>
const Text("Could not load saved view."), //TODO: INTL
orElse: () => const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),
),
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
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 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.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/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/theme.dart';
class ColorSchemeOptionSetting extends StatelessWidget {
const ColorSchemeOptionSetting({super.key});
@@ -52,10 +54,10 @@ class ColorSchemeOptionSetting extends StatelessWidget {
initialValue: settings.preferredColorSchemeOption,
),
).then(
(value) {
(value) async {
if (value != null) {
settings.preferredColorSchemeOption = value;
settings.save();
await settings.save();
}
},
),

View File

@@ -1,7 +1,9 @@
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/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/theme.dart';
class ThemeModeSetting extends StatelessWidget {
const ThemeModeSetting({super.key});
@@ -34,10 +36,10 @@ class ThemeModeSetting extends StatelessWidget {
)
],
),
).then((value) {
).then((value) async {
if (value != null) {
settings.preferredThemeMode = value;
settings.save();
await settings.save();
}
}),
);

View File

@@ -67,7 +67,7 @@
"@startTyping": {},
"doYouReallyWantToDeleteThisView": "Vols esborrar aquesta vista?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Esborra Vista ",
"deleteView": "Esborra Vista {name}?",
"@deleteView": {},
"addedAt": "Afegit",
"@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": {
"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": {},
"doYouReallyWantToDeleteThisView": "Opravdu chceš tento náhled smazat?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Smazat náhled ",
"deleteView": "Smazat náhled {name}?",
"@deleteView": {},
"addedAt": "Přidáno",
"@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": {
"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": {},
"doYouReallyWantToDeleteThisView": "Möchtest Du diese Ansicht wirklich löschen?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Lösche Ansicht ",
"deleteView": "Ansicht {name} löschen?",
"@deleteView": {},
"addedAt": "Hinzugefügt am",
"@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": {
"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": {},
"doYouReallyWantToDeleteThisView": "Do you really want to delete this view?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Delete view ",
"deleteView": "Delete view {name}?",
"@deleteView": {},
"addedAt": "Added at",
"@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": {
"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": {
"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": {},
"doYouReallyWantToDeleteThisView": "Voulez-vous vraiment supprimer cette vue enregistrée ?",
"@doYouReallyWantToDeleteThisView": {},
"deleteView": "Supprimer la vue enregistrée ",
"deleteView": "Supprimer la vue enregistrée {name}?",
"@deleteView": {},
"addedAt": "Date dajout",
"@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": {
"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": {
"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": {
"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": {
"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:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_displaymode/flutter_displaymode.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/services/authentication_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
@@ -109,7 +109,6 @@ void main() async {
if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo;
}
final connectivity = Connectivity();
final localAuthentication = LocalAuthentication();
final connectivityStatusService =
@@ -149,6 +148,7 @@ void main() async {
final authenticationCubit =
AuthenticationCubit(localAuthService, apiFactory, sessionManager);
await authenticationCubit.restoreSessionState();
runApp(
MultiProvider(
providers: [
@@ -228,11 +228,11 @@ class _GoRouterShellState extends State<GoRouterShell> {
$loginRoute,
$verifyIdentityRoute,
$switchingAccountsRoute,
$settingsRoute,
ShellRoute(
navigatorKey: rootNavigatorKey,
builder: ProviderShellRoute(widget.apiFactory).build,
routes: [
$settingsRoute,
$savedViewsRoute,
StatefulShellRoute(
navigatorContainerBuilder: (context, navigationShell, children) {

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.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/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
import 'package:paperless_mobile/theme.dart';
part 'documents_route.g.dart';
@@ -92,14 +94,21 @@ class EditDocumentRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return BlocProvider(
create: (context) => DocumentEditCubit(
context.read(),
context.read(),
context.read(),
document: $extra,
)..loadFieldSuggestions(),
child: const DocumentEditPage(),
final theme = Theme.of(context);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: buildOverlayStyle(
theme,
systemNavigationBarColor: theme.colorScheme.background,
),
child: BlocProvider(
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>(
path: "/landing",
name: R.landing,
routes: [
TypedGoRoute<SavedViewRoute>(
path: "saved-view",
name: R.savedView,
),
],
)
class LandingRoute extends GoRouteData {
const LandingRoute();
@@ -29,10 +23,3 @@ class LandingRoute extends GoRouteData {
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/services.dart';
import 'package:go_router/go_router.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/theme.dart';
part 'settings_route.g.dart';
@@ -10,8 +13,16 @@ part 'settings_route.g.dart';
name: R.settings,
)
class SettingsRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
const _classicThemeColorSeed = Colors.lightGreen;
@@ -46,6 +47,12 @@ ThemeData buildTheme({
colorScheme: colorScheme.harmonized(),
useMaterial3: true,
).copyWith(
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: colorScheme.surface,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: colorScheme.surface,
),
cardTheme: _defaultCardTheme,
inputDecorationTheme: _defaultInputDecorationTheme,
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
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final result = await getCollection(
"/api/saved_views/",
"/api/saved_views/?page_size=100000",
SavedView.fromJson,
ErrorCode.loadSavedViewsError,
client: _client,

View File

@@ -345,6 +345,14 @@ packages:
url: "https://pub.dev"
source: hosted
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: "direct dev"
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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 2.3.11+46
version: 2.3.12+47
environment:
sdk: ">=3.0.0 <4.0.0"
@@ -93,6 +93,7 @@ dependencies:
go_router: ^10.0.0
fl_chart: ^0.63.0
palette_generator: ^0.3.3+2
defer_pointer: ^0.0.2
dependency_overrides:
intl: ^0.18.1