feat: Improve container opening animation, improve scrolling on details page

This commit is contained in:
Anton Stubenbord
2023-04-07 19:00:25 +02:00
parent 10d48e6a55
commit 7299ff9ef1
10 changed files with 548 additions and 226 deletions

View File

@@ -0,0 +1,291 @@
// MIT License
//
// Copyright (c) 2019 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(
BuildContext context, ChipsInputState<T> state, T data);
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
super.key,
this.decoration = const InputDecoration(),
required this.chipBuilder,
required this.suggestionBuilder,
required this.findSuggestions,
required this.onChanged,
this.onChipTapped,
});
final InputDecoration decoration;
final ChipsInputSuggestions<T> findSuggestions;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T>? onChipTapped;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
@override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>> {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = {};
List<T> _suggestions = [];
int _searchId = 0;
FocusNode _focusNode = FocusNode();
TextEditingValue _value = const TextEditingValue();
TextInputConnection? _connection;
String get text {
return String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
}
TextEditingValue get currentTextEditingValue => _value;
bool get _hasInputConnection =>
_connection != null && (_connection?.attached ?? false);
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void selectSuggestion(T data) {
setState(() {
_chips.add(data);
_updateTextInputState();
_suggestions = [];
});
widget.onChanged(_chips.toList(growable: false));
}
void deleteChip(T data) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
widget.onChanged(_chips.toList(growable: false));
}
@override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {
// rebuild so that _TextCursor is hidden.
});
}
@override
void dispose() {
_focusNode.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection?.setEditingState(_value);
}
_connection?.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection?.close();
_connection = null;
}
}
@override
Widget build(BuildContext context) {
var chipsChildren = _chips
.map<Widget>(
(data) => widget.chipBuilder(context, this, data),
)
.toList();
final theme = Theme.of(context);
chipsChildren.add(
SizedBox(
height: 32.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
text,
style: theme.textTheme.bodyLarge?.copyWith(
height: 1.5,
),
),
_TextCaret(
resumed: _focusNode.hasFocus,
),
],
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
//mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.isEmpty,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
Expanded(
child: ListView.builder(
itemCount: _suggestions.length,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(
context, this, _suggestions[index]);
},
),
),
],
);
}
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
}
_value = value;
});
_onSearchChanged(text);
}
int _countReplacements(TextEditingValue value) {
return value.text.codeUnits
.where((ch) => ch == kObjectReplacementChar)
.length;
}
void _updateTextInputState() {
final text =
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
composing: TextRange(start: 0, end: text.length),
);
_connection?.setEditingState(_value);
}
void _onSearchChanged(String value) async {
final localId = ++_searchId;
final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) {
setState(() => _suggestions = results
.where((profile) => !_chips.contains(profile))
.toList(growable: false));
}
}
}
class _TextCaret extends StatefulWidget {
const _TextCaret({
this.duration = const Duration(milliseconds: 500),
this.resumed = false,
});
final Duration duration;
final bool resumed;
@override
_TextCursorState createState() => _TextCursorState();
}
class _TextCursorState extends State<_TextCaret>
with SingleTickerProviderStateMixin {
bool _displayed = false;
late Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(widget.duration, _onTimer);
}
void _onTimer(Timer timer) {
setState(() => _displayed = !_displayed);
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
heightFactor: 0.7,
child: Opacity(
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
child: Container(
width: 2.0,
color: theme.primaryColor,
),
),
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
class BulkEditPage<int, T extends Label> extends StatefulWidget { class BulkEditPage<T extends Label> extends StatefulWidget {
final bool enableMultipleChoice; final bool enableMultipleChoice;
final Map<int, T> availableOptions; final Map<int, T> availableOptions;

View File

@@ -5,7 +5,7 @@ import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/material/search/colored_tab_bar.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart';
@@ -42,6 +42,8 @@ class DocumentDetailsPage extends StatefulWidget {
class _DocumentDetailsPageState extends State<DocumentDetailsPage> { class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
late Future<DocumentMetaData> _metaData; late Future<DocumentMetaData> _metaData;
static const double _itemSpacing = 24; static const double _itemSpacing = 24;
final _pagingScrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -79,95 +81,100 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
bottomNavigationBar: _buildBottomAppBar(), bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar( SliverOverlapAbsorber(
title: Text(context handle:
.watch<DocumentDetailsCubit>() NestedScrollView.sliverOverlapAbsorberHandleFor(context),
.state sliver: SliverAppBar(
.document title: Text(context
.title), .watch<DocumentDetailsCubit>()
leading: const BackButton(), .state
pinned: true, .document
forceElevated: innerBoxIsScrolled, .title),
collapsedHeight: kToolbarHeight, leading: const BackButton(),
expandedHeight: 250.0, pinned: true,
flexibleSpace: FlexibleSpaceBar( forceElevated: innerBoxIsScrolled,
background: Stack( collapsedHeight: kToolbarHeight,
alignment: Alignment.topCenter, expandedHeight: 250.0,
children: [ flexibleSpace: FlexibleSpaceBar(
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( background: Stack(
builder: (context, state) => Positioned.fill( alignment: Alignment.topCenter,
child: DocumentPreview( children: [
document: state.document, BlocBuilder<DocumentDetailsCubit,
fit: BoxFit.cover, DocumentDetailsState>(
), builder: (context, state) => Positioned.fill(
), child: DocumentPreview(
), document: state.document,
Positioned.fill( fit: BoxFit.cover,
top: 0,
child: Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black.withOpacity(0.7),
Colors.black.withOpacity(0.2),
Colors.transparent,
Colors.transparent,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
), ),
), ),
), ),
), Positioned.fill(
], top: 0,
child: Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black.withOpacity(0.7),
Colors.black.withOpacity(0.2),
Colors.transparent,
Colors.transparent,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
),
],
),
), ),
), bottom: ColoredTabBar(
bottom: ColoredTabBar( tabBar: TabBar(
tabBar: TabBar( isScrollable: true,
isScrollable: true, tabs: [
tabs: [ Tab(
Tab( child: Text(
child: Text( S.of(context)!.overview,
S.of(context)!.overview, style: TextStyle(
style: TextStyle( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .onPrimaryContainer,
.onPrimaryContainer, ),
), ),
), ),
), Tab(
Tab( child: Text(
child: Text( S.of(context)!.content,
S.of(context)!.content, style: TextStyle(
style: TextStyle( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .onPrimaryContainer,
.onPrimaryContainer, ),
), ),
), ),
), Tab(
Tab( child: Text(
child: Text( S.of(context)!.metaData,
S.of(context)!.metaData, style: TextStyle(
style: TextStyle( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .onPrimaryContainer,
.onPrimaryContainer, ),
), ),
), ),
), Tab(
Tab( child: Text(
child: Text( S.of(context)!.similarDocuments,
S.of(context)!.similarDocuments, style: TextStyle(
style: TextStyle( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .onPrimaryContainer,
.onPrimaryContainer, ),
), ),
), ),
), ],
], ),
), ),
), ),
), ),
@@ -181,29 +188,70 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
context.read(), context.read(),
documentId: state.document.id, documentId: state.document.id,
), ),
child: TabBarView( child: Padding(
children: [ padding: const EdgeInsets.symmetric(
DocumentOverviewWidget( vertical: 16,
document: state.document, horizontal: 16,
itemSpacing: _itemSpacing, ),
queryString: widget.titleAndContentQueryString, child: TabBarView(
availableCorrespondents: state.correspondents, children: [
availableDocumentTypes: state.documentTypes, CustomScrollView(
availableTags: state.tags, slivers: [
availableStoragePaths: state.storagePaths, SliverOverlapInjector(
), handle: NestedScrollView
DocumentContentWidget( .sliverOverlapAbsorberHandleFor(context),
isFullContentLoaded: state.isFullContentLoaded, ),
document: state.document, DocumentOverviewWidget(
fullContent: state.fullContent, document: state.document,
queryString: widget.titleAndContentQueryString, itemSpacing: _itemSpacing,
), queryString: widget.titleAndContentQueryString,
DocumentMetaDataWidget( availableCorrespondents: state.correspondents,
document: state.document, availableDocumentTypes: state.documentTypes,
itemSpacing: _itemSpacing, availableTags: state.tags,
), availableStoragePaths: state.storagePaths,
const SimilarDocumentsView(), ),
], ],
),
CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentContentWidget(
isFullContentLoaded: state.isFullContentLoaded,
document: state.document,
fullContent: state.fullContent,
queryString: widget.titleAndContentQueryString,
),
],
),
CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentMetaDataWidget(
document: state.document,
itemSpacing: _itemSpacing,
),
],
),
CustomScrollView(
controller: _pagingScrollController,
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
SimilarDocumentsView(
pagingScrollController: _pagingScrollController,
),
],
),
],
),
), ),
); );
}, },

View File

@@ -20,11 +20,7 @@ class DocumentContentWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SliverToBoxAdapter(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@@ -31,50 +31,43 @@ class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
if (state.metaData == null) { if (state.metaData == null) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return SingleChildScrollView( return SliverList(
child: Padding( delegate: SliverChildListDelegate(
padding: const EdgeInsets.symmetric( [
vertical: 16, ArchiveSerialNumberField(
horizontal: 16, document: widget.document,
), ).paddedOnly(bottom: widget.itemSpacing),
child: Column( DetailsItem.text(
crossAxisAlignment: CrossAxisAlignment.start, DateFormat().format(widget.document.modified),
children: [ context: context,
ArchiveSerialNumberField( label: S.of(context)!.modifiedAt,
document: widget.document, ).paddedOnly(bottom: widget.itemSpacing),
).paddedOnly(bottom: widget.itemSpacing), DetailsItem.text(
DetailsItem.text( DateFormat().format(widget.document.added),
DateFormat().format(widget.document.modified), context: context,
context: context, label: S.of(context)!.addedAt,
label: S.of(context)!.modifiedAt, ).paddedOnly(bottom: widget.itemSpacing),
).paddedOnly(bottom: widget.itemSpacing), DetailsItem.text(
DetailsItem.text( state.metaData!.mediaFilename,
DateFormat().format(widget.document.added), context: context,
context: context, label: S.of(context)!.mediaFilename,
label: S.of(context)!.addedAt, ).paddedOnly(bottom: widget.itemSpacing),
).paddedOnly(bottom: widget.itemSpacing), DetailsItem.text(
DetailsItem.text( state.metaData!.originalChecksum,
state.metaData!.mediaFilename, context: context,
context: context, label: S.of(context)!.originalMD5Checksum,
label: S.of(context)!.mediaFilename, ).paddedOnly(bottom: widget.itemSpacing),
).paddedOnly(bottom: widget.itemSpacing), DetailsItem.text(
DetailsItem.text( formatBytes(state.metaData!.originalSize, 2),
state.metaData!.originalChecksum, context: context,
context: context, label: S.of(context)!.originalFileSize,
label: S.of(context)!.originalMD5Checksum, ).paddedOnly(bottom: widget.itemSpacing),
).paddedOnly(bottom: widget.itemSpacing), DetailsItem.text(
DetailsItem.text( state.metaData!.originalMimeType,
formatBytes(state.metaData!.originalSize, 2), context: context,
context: context, label: S.of(context)!.originalMIMEType,
label: S.of(context)!.originalFileSize, ).paddedOnly(bottom: widget.itemSpacing),
).paddedOnly(bottom: widget.itemSpacing), ],
DetailsItem.text(
state.metaData!.originalMimeType,
context: context,
label: S.of(context)!.originalMIMEType,
).paddedOnly(bottom: widget.itemSpacing),
],
),
), ),
); );
}, },

View File

@@ -30,68 +30,66 @@ class DocumentOverviewWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return SliverList(
padding: const EdgeInsets.symmetric( delegate: SliverChildListDelegate(
vertical: 16, [
horizontal: 16, DetailsItem(
), label: S.of(context)!.title,
children: [ content: HighlightedText(
DetailsItem( text: document.title,
label: S.of(context)!.title, highlights: queryString?.split(" ") ?? [],
content: HighlightedText(
text: document.title,
highlights: queryString?.split(" ") ?? [],
style: Theme.of(context).textTheme.bodyLarge,
),
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
DateFormat.yMMMMd().format(document.created),
context: context,
label: S.of(context)!.createdAt,
).paddedOnly(bottom: itemSpacing),
Visibility(
visible: document.documentType != null,
child: DetailsItem(
label: S.of(context)!.documentType,
content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
label: availableDocumentTypes[document.documentType],
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
), DetailsItem.text(
Visibility( DateFormat.yMMMMd().format(document.created),
visible: document.correspondent != null, context: context,
child: DetailsItem( label: S.of(context)!.createdAt,
label: S.of(context)!.correspondent,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
label: availableCorrespondents[document.correspondent],
),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
), Visibility(
Visibility( visible: document.documentType != null,
visible: document.storagePath != null, child: DetailsItem(
child: DetailsItem( label: S.of(context)!.documentType,
label: S.of(context)!.storagePath, content: LabelText<DocumentType>(
content: LabelText<StoragePath>( style: Theme.of(context).textTheme.bodyLarge,
label: availableStoragePaths[document.storagePath], label: availableDocumentTypes[document.documentType],
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.tags.isNotEmpty,
child: DetailsItem(
label: S.of(context)!.tags,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: false,
tags: document.tags.map((e) => availableTags[e]!).toList(),
), ),
), ).paddedOnly(bottom: itemSpacing),
).paddedOnly(bottom: itemSpacing), ),
), Visibility(
], visible: document.correspondent != null,
child: DetailsItem(
label: S.of(context)!.correspondent,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
label: availableCorrespondents[document.correspondent],
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.storagePath != null,
child: DetailsItem(
label: S.of(context)!.storagePath,
content: LabelText<StoragePath>(
label: availableStoragePaths[document.storagePath],
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.tags.isNotEmpty,
child: DetailsItem(
label: S.of(context)!.tags,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: false,
tags: document.tags.map((e) => availableTags[e]!).toList(),
),
),
).paddedOnly(bottom: itemSpacing),
),
],
),
); );
} }
} }

View File

@@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/widgets/material/search/colored_tab_bar.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';

View File

@@ -4,7 +4,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/material/search/colored_tab_bar.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.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/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';

View File

@@ -7,11 +7,13 @@ import 'package:paperless_mobile/features/documents/view/widgets/adaptive_docume
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart'; import 'package:paperless_mobile/routes/document_details_route.dart';
class SimilarDocumentsView extends StatefulWidget { class SimilarDocumentsView extends StatefulWidget {
const SimilarDocumentsView({super.key}); final ScrollController pagingScrollController;
const SimilarDocumentsView({super.key, required this.pagingScrollController});
@override @override
State<SimilarDocumentsView> createState() => _SimilarDocumentsViewState(); State<SimilarDocumentsView> createState() => _SimilarDocumentsViewState();
@@ -20,8 +22,7 @@ class SimilarDocumentsView extends StatefulWidget {
class _SimilarDocumentsViewState extends State<SimilarDocumentsView> class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
with DocumentPagingViewMixin<SimilarDocumentsView, SimilarDocumentsCubit> { with DocumentPagingViewMixin<SimilarDocumentsView, SimilarDocumentsCubit> {
@override @override
final pagingScrollController = ScrollController(); ScrollController get pagingScrollController => widget.pagingScrollController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -43,25 +44,20 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>( return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
builder: (context, state) { builder: (context, state) {
if (!connectivity.isConnected && !state.hasLoaded) { if (!connectivity.isConnected && !state.hasLoaded) {
return const OfflineWidget(); return const SliverToBoxAdapter(
child: OfflineWidget(),
);
} }
if (state.hasLoaded && if (state.hasLoaded &&
!state.isLoading && !state.isLoading &&
state.documents.isEmpty) { state.documents.isEmpty) {
return DocumentsEmptyState( return SliverToBoxAdapter(
state: state, child: Center(
onReset: () => context child: Text(S.of(context)!.noItemsFound),
.read<SimilarDocumentsCubit>() ),
.updateFilter(
filter: DocumentFilter.initial.copyWith(
moreLike: () =>
context.read<SimilarDocumentsCubit>().documentId,
),
),
); );
} }
return DefaultAdaptiveDocumentsView( return SliverAdaptiveDocumentsView(
scrollController: pagingScrollController,
documents: state.documents, documents: state.documents,
hasInternetConnection: connectivity.isConnected, hasInternetConnection: connectivity.isConnected,
isLabelClickable: false, isLabelClickable: false,