mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 00:07:48 -06:00
feat: Improve container opening animation, improve scrolling on details page
This commit is contained in:
291
lib/core/widgets/material/chips_input.dart
Normal file
291
lib/core/widgets/material/chips_input.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user