feat: Migrations, new saved views interface

This commit is contained in:
Anton Stubenbord
2023-09-19 01:50:02 +02:00
parent 2e8144700f
commit f3560f00ea
31 changed files with 1745 additions and 376 deletions

View File

@@ -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.");
}
}

View File

@@ -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),
);
}

View File

@@ -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,

View File

@@ -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(),
);
}
}

View File

@@ -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),
),
],
),
);
}

View File

@@ -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),