mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 13:15:55 -06:00
feat: Update translations, finish saved views rework, some other fixes
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
31
lib/core/widgets/dialog_utils/pop_with_unsaved_changes.dart
Normal file
31
lib/core/widgets/dialog_utils/pop_with_unsaved_changes.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 ...[]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -130,7 +130,7 @@ class LocalNotificationService {
|
||||
filePath: filePath,
|
||||
).toJson(),
|
||||
),
|
||||
); //TODO: INTL
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 d’ajout",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,3 +6,4 @@ final documentsNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final scannerNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final labelsNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final inboxNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final settingsNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user