mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 14:07:59 -06:00
feat: Migrations, new saved views interface
This commit is contained in:
@@ -1,17 +1,11 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AppDrawer extends StatelessWidget {
|
||||
@@ -61,27 +55,40 @@ class AppDrawer extends StatelessWidget {
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(left: 3),
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/bmc-logo.svg',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
leading: const Icon(Icons.favorite_outline),
|
||||
title: Text(S.of(context)!.donate),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
icon: const Icon(Icons.favorite),
|
||||
title: Text(S.of(context)!.donate),
|
||||
content: Text(
|
||||
S.of(context)!.donationDialogContent,
|
||||
),
|
||||
actions: const [
|
||||
Text("~ Anton"),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: SvgPicture.asset(
|
||||
"assets/images/github-mark.svg",
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context)!.donateCoffee),
|
||||
const Icon(
|
||||
Icons.open_in_new,
|
||||
size: 16,
|
||||
)
|
||||
],
|
||||
title: Text(S.of(context)!.sourceCode),
|
||||
trailing: const Icon(
|
||||
Icons.open_in_new,
|
||||
size: 16,
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString(
|
||||
"https://www.buymeacoffee.com/astubenbord",
|
||||
"https://github.com/astubenbord/paperless-mobile",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:badges/badges.dart' as b;
|
||||
import 'package:collection/collection.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';
|
||||
@@ -13,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_changed_dialog.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_views_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||
@@ -45,29 +47,13 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
|
||||
final SliverOverlapAbsorberHandle savedViewsHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
late final TabController _tabController;
|
||||
|
||||
int _currentTab = 0;
|
||||
|
||||
bool get hasSelectedViewChanged {
|
||||
final cubit = context.watch<DocumentsCubit>();
|
||||
final savedViewCubit = context.watch<SavedViewCubit>();
|
||||
final activeView = savedViewCubit.state.maybeMap(
|
||||
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;
|
||||
return viewHasChanged;
|
||||
}
|
||||
|
||||
final _savedViewsExpansionController = ExpansionTileController();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -177,21 +163,14 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
animationType: b.BadgeAnimationType.fade,
|
||||
badgeColor: Colors.red,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: (_currentTab == 0)
|
||||
? FloatingActionButton(
|
||||
heroTag: "fab_documents_page_filter",
|
||||
child:
|
||||
const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
)
|
||||
: FloatingActionButton(
|
||||
heroTag: "fab_documents_page_filter",
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () =>
|
||||
_onCreateSavedView(state.filter),
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
})),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -236,7 +215,10 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
SliverOverlapAbsorber(
|
||||
handle: savedViewsHandle,
|
||||
sliver: SliverPinnedHeader(
|
||||
child: _buildViewActions(),
|
||||
child: Material(
|
||||
child: _buildViewActions(),
|
||||
elevation: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
// SliverOverlapAbsorber(
|
||||
@@ -288,70 +270,13 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
physics: context
|
||||
.watch<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (context
|
||||
.watch<LocalUserAccount>()
|
||||
.paperlessUser
|
||||
.canViewSavedViews)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildSavedViewsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: hasSelectedViewChanged ? 1 : 0,
|
||||
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: () {},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text(
|
||||
"Update selected view",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildSavedViewChangedIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -362,29 +287,82 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverOverlapInjector(
|
||||
handle: savedViewsHandle,
|
||||
),
|
||||
const SavedViewList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -393,6 +371,10 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
onNotification: (notification) {
|
||||
// Listen for scroll notifications to load new data.
|
||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||
final offset = notification.metrics.pixels;
|
||||
if (offset > 128 && _savedViewsExpansionController.isExpanded) {
|
||||
_savedViewsExpansionController.collapse();
|
||||
}
|
||||
|
||||
final currState = context.read<DocumentsCubit>().state;
|
||||
final max = notification.metrics.maxScrollExtent;
|
||||
@@ -403,7 +385,6 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
return false;
|
||||
}
|
||||
|
||||
final offset = notification.metrics.pixels;
|
||||
if (offset >= max * 0.7) {
|
||||
context
|
||||
.read<DocumentsCubit>()
|
||||
@@ -420,7 +401,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
return false;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
edgeOffset: kTextTabBarHeight,
|
||||
edgeOffset: kTextTabBarHeight + 2,
|
||||
onRefresh: _onReloadDocuments,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: CustomScrollView(
|
||||
@@ -434,6 +415,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
builder: (context, state) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SavedViewsWidget(
|
||||
controller: _savedViewsExpansionController,
|
||||
onViewSelected: (view) {
|
||||
final cubit = context.read<DocumentsCubit>();
|
||||
if (state.filter.selectedView == view.id) {
|
||||
@@ -449,6 +431,22 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
showSnackBar(context,
|
||||
"Saved view successfully updated."); //TODO: INTL
|
||||
},
|
||||
onDeleteView: (view) async {
|
||||
HapticFeedback.mediumImpact();
|
||||
final shouldRemove = await showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ConfirmDeleteSavedViewDialog(view: view),
|
||||
);
|
||||
if (shouldRemove) {
|
||||
final documentsCubit = context.read<DocumentsCubit>();
|
||||
context.read<SavedViewCubit>().remove(view);
|
||||
if (documentsCubit.state.filter.selectedView ==
|
||||
view.id) {
|
||||
documentsCubit.resetFilter();
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: state.filter,
|
||||
),
|
||||
);
|
||||
@@ -722,16 +720,10 @@ 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 context.read<DocumentsCubit>().reload();
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onReloadSavedViews() async {
|
||||
try {
|
||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||
await context.read<SavedViewCubit>().reload();
|
||||
await Future.wait([
|
||||
context.read<DocumentsCubit>().reload(),
|
||||
context.read<SavedViewCubit>().reload(),
|
||||
]);
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
@@ -754,22 +746,37 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
if (viewHasChanged) {
|
||||
final discardChanges = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => SavedViewChangedDialog(),
|
||||
builder: (context) => const SavedViewChangedDialog(),
|
||||
);
|
||||
if (discardChanges == true) {
|
||||
cubit.resetFilter();
|
||||
// Reset
|
||||
} else if (discardChanges == false) {
|
||||
final newView = activeView.copyWith(
|
||||
filterRules: FilterRule.fromFilter(cubit.state.filter),
|
||||
);
|
||||
final savedViewCubit2 = context.read<SavedViewCubit>();
|
||||
|
||||
await savedViewCubit2.update(newView);
|
||||
showSnackBar(context, "Saved view successfully updated.");
|
||||
_updateCurrentSavedView();
|
||||
}
|
||||
} else {
|
||||
cubit.resetFilter();
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,92 +31,90 @@ class DocumentListItem extends DocumentItem {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final labels = context.watch<LabelRepository>().state;
|
||||
return Material(
|
||||
child: ListTile(
|
||||
tileColor: backgroundColor,
|
||||
dense: true,
|
||||
selected: isSelected,
|
||||
onTap: () => _onTap(),
|
||||
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
onLongPress: () => onSelected?.call(document),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
child: CorrespondentWidget(
|
||||
isClickable: isLabelClickable,
|
||||
correspondent: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.correspondents[document.correspondent],
|
||||
onSelected: onCorrespondentSelected,
|
||||
),
|
||||
return ListTile(
|
||||
tileColor: backgroundColor,
|
||||
dense: true,
|
||||
selected: isSelected,
|
||||
onTap: () => _onTap(),
|
||||
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
child: CorrespondentWidget(
|
||||
isClickable: isLabelClickable,
|
||||
correspondent: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.correspondents[document.correspondent],
|
||||
onSelected: onCorrespondentSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
document.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
child: TagsWidget(
|
||||
isClickable: isLabelClickable,
|
||||
tags: document.tags
|
||||
.where((e) => labels.tags.containsKey(e))
|
||||
.map((e) => labels.tags[e]!)
|
||||
.toList(),
|
||||
onTagSelected: (id) => onTagSelected?.call(id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
],
|
||||
),
|
||||
Text(
|
||||
document.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: DateFormat.yMMMd().format(document.created),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.apply(color: Colors.grey),
|
||||
children: document.documentType != null
|
||||
? [
|
||||
const TextSpan(text: '\u30FB'),
|
||||
TextSpan(
|
||||
text: labels.documentTypes[document.documentType]?.name,
|
||||
recognizer: onDocumentTypeSelected != null
|
||||
? (TapGestureRecognizer()
|
||||
..onTap = () => onDocumentTypeSelected!(
|
||||
document.documentType))
|
||||
: null,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
maxLines: 1,
|
||||
),
|
||||
AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
child: TagsWidget(
|
||||
isClickable: isLabelClickable,
|
||||
tags: document.tags
|
||||
.where((e) => labels.tags.containsKey(e))
|
||||
.map((e) => labels.tags[e]!)
|
||||
.toList(),
|
||||
onTagSelected: (id) => onTagSelected?.call(id),
|
||||
),
|
||||
),
|
||||
),
|
||||
isThreeLine: document.tags.isNotEmpty,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: _a4AspectRatio,
|
||||
child: GestureDetector(
|
||||
child: DocumentPreview(
|
||||
document: document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: enableHeroAnimation,
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(8.0),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: DateFormat.yMMMd().format(document.created),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.apply(color: Colors.grey),
|
||||
children: document.documentType != null
|
||||
? [
|
||||
const TextSpan(text: '\u30FB'),
|
||||
TextSpan(
|
||||
text: labels.documentTypes[document.documentType]?.name,
|
||||
recognizer: onDocumentTypeSelected != null
|
||||
? (TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
onDocumentTypeSelected!(document.documentType))
|
||||
: null,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
isThreeLine: document.tags.isNotEmpty,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: _a4AspectRatio,
|
||||
child: GestureDetector(
|
||||
child: DocumentPreview(
|
||||
document: document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: enableHeroAnimation,
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(8.0),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ class SavedViewChangedDialog extends StatelessWidget {
|
||||
actionsOverflowButtonSpacing: 8,
|
||||
actions: [
|
||||
const DialogCancelButton(),
|
||||
TextButton(
|
||||
child: Text(S.of(context)!.saveChanges),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
),
|
||||
// TextButton(
|
||||
// child: Text(S.of(context)!.saveChanges),
|
||||
// onPressed: () {
|
||||
// Navigator.pop(context, false);
|
||||
// },
|
||||
// ),
|
||||
DialogConfirmButton(
|
||||
label: S.of(context)!.resetFilter,
|
||||
style: DialogConfirmButtonStyle.danger,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class SavedViewChip extends StatelessWidget {
|
||||
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 {
|
||||
final SavedView view;
|
||||
final void Function(SavedView view) onViewSelected;
|
||||
final void Function(SavedView vie) onUpdateView;
|
||||
final void Function(SavedView view) onUpdateView;
|
||||
final void Function(SavedView view) onDeleteView;
|
||||
final bool selected;
|
||||
final bool hasChanged;
|
||||
|
||||
@@ -16,28 +21,146 @@ class SavedViewChip extends StatelessWidget {
|
||||
required this.selected,
|
||||
required this.hasChanged,
|
||||
required this.onUpdateView,
|
||||
required this.onDeleteView,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SavedViewChip> createState() => _SavedViewChipState();
|
||||
}
|
||||
|
||||
class _SavedViewChipState extends State<SavedViewChip>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_animation = _animationController.drive(Tween(begin: 0, end: 1));
|
||||
}
|
||||
|
||||
bool _isExpanded = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Badge(
|
||||
smallSize: 12,
|
||||
alignment: const AlignmentDirectional(1.1, -1.2),
|
||||
backgroundColor: Colors.red,
|
||||
isLabelVisible: hasChanged,
|
||||
child: FilterChip(
|
||||
avatar: Icon(
|
||||
Icons.saved_search,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
var colorScheme = Theme.of(context).colorScheme;
|
||||
final effectiveBackgroundColor = widget.selected
|
||||
? colorScheme.secondaryContainer
|
||||
: colorScheme.surfaceVariant;
|
||||
final effectiveForegroundColor = widget.selected
|
||||
? colorScheme.onSecondaryContainer
|
||||
: colorScheme.onSurfaceVariant;
|
||||
|
||||
final expandedChild = Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: effectiveForegroundColor,
|
||||
),
|
||||
onPressed: () {
|
||||
EditSavedViewRoute(widget.view).push(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
onPressed: () async {
|
||||
widget.onDeleteView(widget.view);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: effectiveBackgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
enableFeedback: true,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => widget.onViewSelected(widget.view),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_buildCheckmark(effectiveForegroundColor),
|
||||
_buildLabel(context, effectiveForegroundColor)
|
||||
.paddedSymmetrically(
|
||||
horizontal: 12,
|
||||
vertical: 0,
|
||||
),
|
||||
],
|
||||
).paddedOnly(left: 8),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
child: _isExpanded ? expandedChild : const SizedBox.shrink(),
|
||||
),
|
||||
_buildTrailing(effectiveForegroundColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
showCheckmark: false,
|
||||
selectedColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
selected: selected,
|
||||
label: Text(view.name),
|
||||
onSelected: (_) {
|
||||
onViewSelected(view);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrailing(Color effectiveForegroundColor) {
|
||||
return IconButton(
|
||||
icon: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _animation.value * pi,
|
||||
child: Icon(
|
||||
_isExpanded ? Icons.close : Icons.chevron_right,
|
||||
color: effectiveForegroundColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
if (_isExpanded) {
|
||||
_animationController.reverse();
|
||||
} else {
|
||||
_animationController.forward();
|
||||
}
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLabel(BuildContext context, Color effectiveForegroundColor) {
|
||||
return Text(
|
||||
widget.view.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(color: effectiveForegroundColor),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckmark(Color effectiveForegroundColor) {
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: widget.selected
|
||||
? Icon(Icons.check, color: effectiveForegroundColor)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,144 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
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/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';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||
|
||||
class SavedViewsWidget extends StatelessWidget {
|
||||
class SavedViewsWidget extends StatefulWidget {
|
||||
final void Function(SavedView view) onViewSelected;
|
||||
final void Function(SavedView view) onUpdateView;
|
||||
final void Function(SavedView view) onDeleteView;
|
||||
|
||||
final DocumentFilter filter;
|
||||
final ExpansionTileController? controller;
|
||||
|
||||
const SavedViewsWidget({
|
||||
super.key,
|
||||
required this.onViewSelected,
|
||||
required this.filter,
|
||||
required this.onUpdateView,
|
||||
required this.onDeleteView,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SavedViewsWidget> createState() => _SavedViewsWidgetState();
|
||||
}
|
||||
|
||||
class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
late final Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_animation = _animationController.drive(Tween(begin: 0, end: 0.5));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
height: 50,
|
||||
child: BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeWhen(
|
||||
loaded: (savedViews) {
|
||||
if (savedViews.isEmpty) {
|
||||
return Text("No saved views");
|
||||
}
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final view = savedViews.values.elementAt(index);
|
||||
return SavedViewChip(
|
||||
view: view,
|
||||
onUpdateView: onUpdateView,
|
||||
onViewSelected: onViewSelected,
|
||||
selected: filter.selectedView != null &&
|
||||
view.id == filter.selectedView,
|
||||
hasChanged: filter.selectedView == view.id &&
|
||||
filter != view.toDocumentFilter(),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: savedViews.length,
|
||||
error: (_) => const Placeholder(),
|
||||
);
|
||||
},
|
||||
error: () => Text("Error loading saved views"),
|
||||
orElse: () => 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),
|
||||
),
|
||||
).padded(4),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
S.of(context)!.deleteView + view.name + "?",
|
||||
S.of(context)!.deleteView(view.name),
|
||||
softWrap: true,
|
||||
),
|
||||
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
||||
|
||||
@@ -37,9 +37,16 @@ class AddTagPage extends StatelessWidget {
|
||||
.withOpacity(1.0),
|
||||
readOnly: true,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
FormBuilderField<bool>(
|
||||
name: Tag.isInboxTagKey,
|
||||
title: Text(S.of(context)!.inboxTag),
|
||||
initialValue: false,
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.inboxTag),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -38,10 +38,16 @@ class EditTagPage extends StatelessWidget {
|
||||
colorPickerType: ColorPickerType.materialPicker,
|
||||
readOnly: true,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
initialValue: tag.isInboxTag,
|
||||
FormBuilderField<bool>(
|
||||
name: Tag.isInboxTagKey,
|
||||
title: Text(S.of(context)!.inboxTag),
|
||||
initialValue: tag.isInboxTag,
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.inboxTag),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -137,10 +137,16 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
initialValue: widget.initialValue?.match,
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
FormBuilderField<bool>(
|
||||
name: Label.isInsensitiveKey,
|
||||
initialValue: widget.initialValue?.isInsensitive ?? true,
|
||||
title: Text(S.of(context)!.caseIrrelevant),
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.caseIrrelevant),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
...widget.additionalFields,
|
||||
].padded(),
|
||||
|
||||
@@ -55,9 +55,17 @@ class _LandingPageState extends State<LandingPage> {
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 0, 8),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
"Saved Views",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.saved_search,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
).paddedOnly(right: 8),
|
||||
Text(
|
||||
S.of(context)!.views,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -75,7 +83,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||
children: [
|
||||
Text(
|
||||
"There are no saved views to show on your dashboard.", //TODO: INTL
|
||||
),
|
||||
).padded(),
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
@@ -89,6 +97,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||
itemBuilder: (context, index) {
|
||||
return SavedViewPreview(
|
||||
savedView: dashboardViews.elementAt(index),
|
||||
expanded: index == 0,
|
||||
);
|
||||
},
|
||||
itemCount: dashboardViews.length,
|
||||
|
||||
@@ -29,7 +29,7 @@ class ExpansionCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
title: title,
|
||||
children: [content],
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
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/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';
|
||||
|
||||
const _fkName = 'name';
|
||||
const _fkShowOnDashboard = 'show_on_dashboard';
|
||||
const _fkShowInSidebar = 'show_in_sidebar';
|
||||
|
||||
class AddSavedViewPage extends StatefulWidget {
|
||||
final DocumentFilter? initialFilter;
|
||||
const AddSavedViewPage({
|
||||
@@ -17,12 +23,7 @@ class AddSavedViewPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
||||
static const fkName = 'name';
|
||||
static const fkShowOnDashboard = 'show_on_dashboard';
|
||||
static const fkShowInSidebar = 'show_in_sidebar';
|
||||
|
||||
final _savedViewFormKey = GlobalKey<FormBuilderState>();
|
||||
final _filterFormKey = GlobalKey<FormBuilderState>();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -46,7 +47,7 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: _AddSavedViewPageState.fkName,
|
||||
name: _fkName,
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
@@ -57,41 +58,53 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
||||
label: Text(S.of(context)!.name),
|
||||
),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: _AddSavedViewPageState.fkShowOnDashboard,
|
||||
FormBuilderField<bool>(
|
||||
name: _fkShowOnDashboard,
|
||||
initialValue: false,
|
||||
title: Text(S.of(context)!.showOnDashboard),
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.showOnDashboard),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: _AddSavedViewPageState.fkShowInSidebar,
|
||||
FormBuilderField<bool>(
|
||||
name: _fkShowInSidebar,
|
||||
initialValue: false,
|
||||
title: Text(S.of(context)!.showInSidebar),
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.showInSidebar),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCreate(BuildContext context) {
|
||||
void _onCreate(BuildContext context) async {
|
||||
if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) {
|
||||
// context.pop(
|
||||
// SavedView.fromDocumentFilter(
|
||||
// DocumentFilterForm.assembleFilter(
|
||||
// _filterFormKey,
|
||||
// widget.currentFilter,
|
||||
// ),
|
||||
// name: _savedViewFormKey.currentState?.value[fkName] as String,
|
||||
// showOnDashboard:
|
||||
// _savedViewFormKey.currentState?.value[fkShowOnDashboard] as bool,
|
||||
// showInSidebar:
|
||||
// _savedViewFormKey.currentState?.value[fkShowInSidebar] as bool,
|
||||
// ),
|
||||
// );
|
||||
final cubit = context.read<SavedViewCubit>();
|
||||
var savedView = SavedView.fromDocumentFilter(
|
||||
widget.initialFilter ?? const DocumentFilter(),
|
||||
name: _savedViewFormKey.currentState?.value[_fkName] as String,
|
||||
showOnDashboard:
|
||||
_savedViewFormKey.currentState?.value[_fkShowOnDashboard] as bool,
|
||||
showInSidebar:
|
||||
_savedViewFormKey.currentState?.value[_fkShowInSidebar] as bool,
|
||||
);
|
||||
final router = GoRouter.of(context);
|
||||
await cubit.add(
|
||||
savedView,
|
||||
);
|
||||
router.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
lib/features/saved_view/view/edit_saved_view_page.dart
Normal file
106
lib/features/saved_view/view/edit_saved_view_page.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
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/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
const _fkName = 'name';
|
||||
const _fkShowOnDashboard = 'show_on_dashboard';
|
||||
const _fkShowInSidebar = 'show_in_sidebar';
|
||||
|
||||
class EditSavedViewPage extends StatefulWidget {
|
||||
final SavedView savedView;
|
||||
const EditSavedViewPage({
|
||||
super.key,
|
||||
required this.savedView,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditSavedViewPage> createState() => _EditSavedViewPageState();
|
||||
}
|
||||
|
||||
class _EditSavedViewPageState extends State<EditSavedViewPage> {
|
||||
final _savedViewFormKey = GlobalKey<FormBuilderState>();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context)!.editView),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
heroTag: "fab_edit_saved_view_page",
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: () => _onCreate(context),
|
||||
label: Text(S.of(context)!.saveChanges),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FormBuilder(
|
||||
key: _savedViewFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
initialValue: widget.savedView.name,
|
||||
name: _fkName,
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.name),
|
||||
),
|
||||
),
|
||||
FormBuilderField<bool>(
|
||||
name: _fkShowOnDashboard,
|
||||
initialValue: widget.savedView.showOnDashboard,
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.showOnDashboard),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
FormBuilderField<bool>(
|
||||
name: _fkShowInSidebar,
|
||||
initialValue: widget.savedView.showInSidebar,
|
||||
builder: (field) {
|
||||
return CheckboxListTile(
|
||||
value: field.value,
|
||||
title: Text(S.of(context)!.showInSidebar),
|
||||
onChanged: (value) => field.didChange(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCreate(BuildContext context) async {
|
||||
if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) {
|
||||
final cubit = context.read<SavedViewCubit>();
|
||||
var savedView = widget.savedView.copyWith(
|
||||
name: _savedViewFormKey.currentState!.value[_fkName],
|
||||
showInSidebar: _savedViewFormKey.currentState!.value[_fkShowInSidebar],
|
||||
showOnDashboard:
|
||||
_savedViewFormKey.currentState!.value[_fkShowOnDashboard],
|
||||
);
|
||||
final router = GoRouter.of(context);
|
||||
await cubit.update(savedView);
|
||||
router.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ import 'package:provider/provider.dart';
|
||||
|
||||
class SavedViewPreview extends StatelessWidget {
|
||||
final SavedView savedView;
|
||||
final bool expanded;
|
||||
const SavedViewPreview({
|
||||
super.key,
|
||||
required this.savedView,
|
||||
required this.expanded,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -24,7 +26,7 @@ class SavedViewPreview extends StatelessWidget {
|
||||
SavedViewPreviewCubit(context.read(), savedView)..initialize(),
|
||||
builder: (context, child) {
|
||||
return ExpansionCard(
|
||||
initiallyExpanded: true,
|
||||
initiallyExpanded: expanded,
|
||||
title: Text(savedView.name),
|
||||
content: BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
|
||||
builder: (context, state) {
|
||||
@@ -33,7 +35,7 @@ class SavedViewPreview extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
if (documents.isEmpty)
|
||||
Text("This view is empty.").padded()
|
||||
Text("This view does not match any documents.").padded()
|
||||
else
|
||||
for (final document in documents)
|
||||
DocumentListItem(
|
||||
@@ -45,6 +47,7 @@ class SavedViewPreview extends StatelessWidget {
|
||||
DocumentDetailsRoute($extra: document)
|
||||
.push(context);
|
||||
},
|
||||
onSelected: null,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@@ -64,7 +67,8 @@ class SavedViewPreview extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
},
|
||||
error: () => const Text("Error loading preview"), //TODO: INTL
|
||||
error: () =>
|
||||
const Text("Could not load saved view."), //TODO: INTL
|
||||
orElse: () => const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
|
||||
@@ -15,9 +15,10 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
||||
static const _languageOptions = {
|
||||
'en': LanguageOption('English', true),
|
||||
'de': LanguageOption('Deutsch', true),
|
||||
'es': LanguageOption("Español", true),
|
||||
'fr': LanguageOption('Français', true),
|
||||
'cs': LanguageOption('Česky', true),
|
||||
'tr': LanguageOption('Türkçe', true),
|
||||
'fr': LanguageOption('Français', true),
|
||||
'pl': LanguageOption('Polska', true),
|
||||
'ca': LanguageOption('Catalan', true),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user