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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';

View File

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

View File

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

View File

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