mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 08:07:59 -06:00
Fixed visual bugs, added notifications on document upload success, enabled editing in inbox, added hints
This commit is contained in:
@@ -54,7 +54,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _onSubmit(state.document),
|
||||
icon: const Icon(Icons.save),
|
||||
label: Text(S.of(context).genericActionSaveLabel),
|
||||
label: Text(S.of(context).genericActionUpdateLabel),
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).documentEditPageTitle),
|
||||
@@ -75,31 +75,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(children: [
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(state.document.created).padded(),
|
||||
_buildDocumentTypeFormField(
|
||||
state.document.documentType,
|
||||
state.documentTypes,
|
||||
).padded(),
|
||||
_buildCorrespondentFormField(
|
||||
state.document.correspondent,
|
||||
state.correspondents,
|
||||
).padded(),
|
||||
_buildStoragePathFormField(
|
||||
state.document.storagePath,
|
||||
state.storagePaths,
|
||||
).padded(),
|
||||
TagFormField(
|
||||
initialValue:
|
||||
IdsTagsQuery.included(state.document.tags.toList()),
|
||||
notAssignedSelectable: false,
|
||||
anyAssignedSelectable: false,
|
||||
excludeAllowed: false,
|
||||
name: fkTags,
|
||||
selectableOptions: state.tags,
|
||||
).padded(),
|
||||
]),
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(state.document.created).padded(),
|
||||
_buildDocumentTypeFormField(
|
||||
state.document.documentType,
|
||||
state.documentTypes,
|
||||
).padded(),
|
||||
_buildCorrespondentFormField(
|
||||
state.document.correspondent,
|
||||
state.correspondents,
|
||||
).padded(),
|
||||
_buildStoragePathFormField(
|
||||
state.document.storagePath,
|
||||
state.storagePaths,
|
||||
).padded(),
|
||||
TagFormField(
|
||||
initialValue:
|
||||
IdsTagsQuery.included(state.document.tags.toList()),
|
||||
notAssignedSelectable: false,
|
||||
anyAssignedSelectable: false,
|
||||
excludeAllowed: false,
|
||||
name: fkTags,
|
||||
selectableOptions: state.tags,
|
||||
suggestions: widget.suggestions.hasSuggestedTags
|
||||
? _buildSuggestionsSkeleton<int>(
|
||||
suggestions: widget.suggestions.storagePaths,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(state.tags[itemData]!.name),
|
||||
onPressed: () {
|
||||
final currentTags = _formKey.currentState
|
||||
?.fields[fkTags] as TagsQuery;
|
||||
if (currentTags is IdsTagsQuery) {
|
||||
_formKey.currentState?.fields[fkTags]
|
||||
?.didChange((IdsTagsQuery.fromIds(
|
||||
[...currentTags.ids, itemData])));
|
||||
} else {
|
||||
_formKey.currentState?.fields[fkTags]
|
||||
?.didChange(
|
||||
(IdsTagsQuery.fromIds([itemData])));
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
).padded(),
|
||||
const SizedBox(
|
||||
height: 64), // Prevent tags from being hidden by fab
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
@@ -267,7 +292,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
_buildSuggestionsSkeleton<DateTime>(
|
||||
suggestions: widget.suggestions.dates,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(DateFormat.yMd().format(itemData)),
|
||||
label: Text(DateFormat.yMMMd().format(itemData)),
|
||||
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
||||
?.didChange(itemData),
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||
@@ -95,156 +96,196 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous != ConnectivityState.connected &&
|
||||
current == ConnectivityState.connected,
|
||||
!previous.isSuccess && current.isSuccess,
|
||||
listener: (context, state) {
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
const linearProgressIndicatorHeight = 4.0;
|
||||
return Scaffold(
|
||||
drawer: BlocProvider.value(
|
||||
value: context.read<AuthenticationCubit>(),
|
||||
child: InfoDrawer(
|
||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
||||
),
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context).documentsPageNewDocumentAvailableText,
|
||||
action: SnackBarActionConfig(
|
||||
label: S
|
||||
.of(context)
|
||||
.documentUploadProcessingSuccessfulReloadActionText,
|
||||
onPressed: () {
|
||||
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
||||
context.read<DocumentsCubit>().reload();
|
||||
},
|
||||
),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(
|
||||
kToolbarHeight + linearProgressIndicatorHeight,
|
||||
duration: const Duration(seconds: 10),
|
||||
);
|
||||
},
|
||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous != ConnectivityState.connected &&
|
||||
current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
const linearProgressIndicatorHeight = 4.0;
|
||||
return Scaffold(
|
||||
drawer: BlocProvider.value(
|
||||
value: context.read<AuthenticationCubit>(),
|
||||
child: InfoDrawer(
|
||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
||||
),
|
||||
),
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return AppBar(
|
||||
title: Text(
|
||||
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
||||
),
|
||||
actions: [
|
||||
const SortDocumentsButton(),
|
||||
BlocBuilder<ApplicationSettingsCubit,
|
||||
ApplicationSettingsState>(
|
||||
builder: (context, settingsState) => IconButton(
|
||||
icon: Icon(
|
||||
settingsState.preferredViewType == ViewType.grid
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
),
|
||||
onPressed: () {
|
||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
||||
setState(() {
|
||||
_offset = 0;
|
||||
_last = 0;
|
||||
});
|
||||
final cubit =
|
||||
context.read<ApplicationSettingsCubit>();
|
||||
cubit.setViewType(
|
||||
cubit.state.preferredViewType.toggle());
|
||||
},
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(
|
||||
kToolbarHeight + linearProgressIndicatorHeight,
|
||||
),
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isEmpty) {
|
||||
return AppBar(
|
||||
title: Text(
|
||||
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
||||
),
|
||||
actions: [
|
||||
const SortDocumentsButton(),
|
||||
BlocBuilder<ApplicationSettingsCubit,
|
||||
ApplicationSettingsState>(
|
||||
builder: (context, settingsState) => IconButton(
|
||||
icon: Icon(
|
||||
settingsState.preferredViewType == ViewType.grid
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
),
|
||||
onPressed: () {
|
||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
||||
setState(() {
|
||||
_offset = 0;
|
||||
_last = 0;
|
||||
});
|
||||
final cubit =
|
||||
context.read<ApplicationSettingsCubit>();
|
||||
cubit.setViewType(
|
||||
cubit.state.preferredViewType.toggle());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(
|
||||
linearProgressIndicatorHeight),
|
||||
child: state.isLoading
|
||||
? const LinearProgressIndicator()
|
||||
: const SizedBox(height: 4.0),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () =>
|
||||
context.read<DocumentsCubit>().resetSelection(),
|
||||
),
|
||||
title: Text(
|
||||
'${state.selection.length} ${S.of(context).documentsSelectedText}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(context, state),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
return b.Badge(
|
||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||
showBadge: appliedFiltersCount > 0,
|
||||
badgeContent: Text(
|
||||
'$appliedFiltersCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize:
|
||||
const Size.fromHeight(linearProgressIndicatorHeight),
|
||||
child: state.isLoading
|
||||
? const LinearProgressIndicator()
|
||||
: const SizedBox(height: 4.0),
|
||||
),
|
||||
animationType: b.BadgeAnimationType.fade,
|
||||
badgeColor: Colors.red,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
return 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: Theme.of(context).colorScheme.error,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||
builder: (context, taskState) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildBody(connectivityState),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: _offset,
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return ColoredBox(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: SavedViewSelectionWidget(
|
||||
height: _savedViewWidgetHeight,
|
||||
currentFilter: state.filter,
|
||||
enabled: state.selection.isEmpty &&
|
||||
connectivityState.isConnected,
|
||||
),
|
||||
);
|
||||
},
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||
builder: (context, taskState) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildBody(connectivityState),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: _offset,
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return ColoredBox(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: SavedViewSelectionWidget(
|
||||
height: _savedViewWidgetHeight,
|
||||
currentFilter: state.filter,
|
||||
enabled: state.selection.isEmpty &&
|
||||
connectivityState.isConnected,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (taskState.task != null &&
|
||||
taskState.isSuccess &&
|
||||
!taskState.task!.acknowledged)
|
||||
_buildNewDocumentAvailableButton(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Align _buildNewDocumentAvailableButton(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: FilledButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Theme.of(context).colorScheme.error),
|
||||
),
|
||||
child: Text("New document available!"),
|
||||
onPressed: () {
|
||||
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
||||
context.read<DocumentsCubit>().reload();
|
||||
},
|
||||
).paddedOnly(bottom: 24, left: 24),
|
||||
);
|
||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
BulkDeleteConfirmationDialog(state: documentsState),
|
||||
) ??
|
||||
false;
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
await context
|
||||
.read<DocumentsCubit>()
|
||||
.bulkRemove(documentsState.selection);
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context).documentsPageBulkDeleteSuccessfulText,
|
||||
);
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _openDocumentFilter() async {
|
||||
|
||||
@@ -42,6 +42,7 @@ class AdaptiveDocumentsView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: beforeItems),
|
||||
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
|
||||
|
||||
@@ -38,7 +38,6 @@ class DocumentListItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
child: ListTile(
|
||||
trailing: Text("${document.id}"),
|
||||
dense: true,
|
||||
selected: isSelected,
|
||||
onTap: () => _onTap(),
|
||||
|
||||
Reference in New Issue
Block a user