mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 21:15:50 -06:00
feat: Implemented new view type, fix connectivity issues, fix offline issues, fix loading animations, fix documents page paging
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -18,7 +19,9 @@ class SessionManager {
|
|||||||
|
|
||||||
static Dio _initDio(List<Interceptor> interceptors) {
|
static Dio _initDio(List<Interceptor> interceptors) {
|
||||||
//en- and decoded by utf8 by default
|
//en- and decoded by utf8 by default
|
||||||
final Dio dio = Dio(BaseOptions());
|
final Dio dio = Dio(
|
||||||
|
BaseOptions(contentType: Headers.jsonContentType),
|
||||||
|
);
|
||||||
dio.options.receiveTimeout = const Duration(seconds: 25);
|
dio.options.receiveTimeout = const Duration(seconds: 25);
|
||||||
dio.options.responseType = ResponseType.json;
|
dio.options.responseType = ResponseType.json;
|
||||||
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
|
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
|
||||||
@@ -71,7 +74,9 @@ class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authToken != null) {
|
if (authToken != null) {
|
||||||
client.options.headers.addAll({'Authorization': 'Token $authToken'});
|
client.options.headers.addAll({
|
||||||
|
HttpHeaders.authorizationHeader: 'Token $authToken',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverInformation != null) {
|
if (serverInformation != null) {
|
||||||
@@ -82,7 +87,7 @@ class SessionManager {
|
|||||||
void resetSettings() {
|
void resetSettings() {
|
||||||
client.httpClientAdapter = IOHttpClientAdapter();
|
client.httpClientAdapter = IOHttpClientAdapter();
|
||||||
client.options.baseUrl = '';
|
client.options.baseUrl = '';
|
||||||
client.options.headers.remove('Authorization');
|
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||||
serverInformation = PaperlessServerInformationModel();
|
serverInformation = PaperlessServerInformationModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,22 @@ extension WidgetPadding on Widget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget paddedSymmetrically({double horizontal = 0.0, double vertical = 0.0}) {
|
Widget paddedSymmetrically({
|
||||||
|
double horizontal = 0.0,
|
||||||
|
double vertical = 0.0,
|
||||||
|
}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
|
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
|
||||||
child: this,
|
child: this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget paddedOnly(
|
Widget paddedOnly({
|
||||||
{double top = 0.0,
|
double top = 0.0,
|
||||||
double bottom = 0.0,
|
double bottom = 0.0,
|
||||||
double left = 0.0,
|
double left = 0.0,
|
||||||
double right = 0.0}) {
|
double right = 0.0,
|
||||||
|
}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: top,
|
top: top,
|
||||||
@@ -30,6 +34,13 @@ extension WidgetPadding on Widget {
|
|||||||
child: this,
|
child: this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget paddedLTRB(double left, double top, double right, double bottom) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(left, top, right, bottom),
|
||||||
|
child: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WidgetsPadding on List<Widget> {
|
extension WidgetsPadding on List<Widget> {
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_loadMetaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadMetaData() {
|
||||||
_metaData = context
|
_metaData = context
|
||||||
.read<PaperlessDocumentsApi>()
|
.read<PaperlessDocumentsApi>()
|
||||||
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
|
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
|
||||||
@@ -64,108 +68,117 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
},
|
},
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 4,
|
length: 4,
|
||||||
child: Scaffold(
|
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
listenWhen: (previous, current) =>
|
||||||
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
!previous.isConnected && current.isConnected,
|
||||||
bottomNavigationBar: _buildBottomAppBar(),
|
listener: (context, state) {
|
||||||
body: NestedScrollView(
|
_loadMetaData();
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
setState(() {});
|
||||||
SliverAppBar(
|
},
|
||||||
leading: const BackButton(),
|
child: Scaffold(
|
||||||
floating: true,
|
floatingActionButtonLocation:
|
||||||
pinned: true,
|
FloatingActionButtonLocation.endDocked,
|
||||||
expandedHeight: 200.0,
|
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
||||||
flexibleSpace:
|
bottomNavigationBar: _buildBottomAppBar(),
|
||||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
body: NestedScrollView(
|
||||||
builder: (context, state) => DocumentPreview(
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
id: state.document.id,
|
SliverAppBar(
|
||||||
fit: BoxFit.cover,
|
leading: const BackButton(),
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
expandedHeight: 200.0,
|
||||||
|
flexibleSpace:
|
||||||
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
builder: (context, state) => DocumentPreview(
|
||||||
|
document: state.document,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottom: ColoredTabBar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
tabBar: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentDetailsPageTabOverviewLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentDetailsPageTabContentLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentDetailsPageTabMetaDataLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S
|
||||||
|
.of(context)
|
||||||
|
.documentDetailsPageTabSimilarDocumentsLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottom: ColoredTabBar(
|
],
|
||||||
backgroundColor:
|
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
builder: (context, state) {
|
||||||
tabBar: TabBar(
|
return BlocProvider(
|
||||||
isScrollable: true,
|
create: (context) => SimilarDocumentsCubit(
|
||||||
tabs: [
|
context.read(),
|
||||||
Tab(
|
context.read(),
|
||||||
child: Text(
|
documentId: state.document.id,
|
||||||
S.of(context).documentDetailsPageTabOverviewLabel,
|
),
|
||||||
style: TextStyle(
|
child: TabBarView(
|
||||||
color: Theme.of(context)
|
children: [
|
||||||
.colorScheme
|
DocumentOverviewWidget(
|
||||||
.onPrimaryContainer,
|
document: state.document,
|
||||||
),
|
itemSpacing: _itemPadding,
|
||||||
|
queryString: widget.titleAndContentQueryString,
|
||||||
),
|
),
|
||||||
),
|
DocumentContentWidget(
|
||||||
Tab(
|
isFullContentLoaded: state.isFullContentLoaded,
|
||||||
child: Text(
|
document: state.document,
|
||||||
S.of(context).documentDetailsPageTabContentLabel,
|
fullContent: state.fullContent,
|
||||||
style: TextStyle(
|
queryString: widget.titleAndContentQueryString,
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
DocumentMetaDataWidget(
|
||||||
Tab(
|
document: state.document,
|
||||||
child: Text(
|
itemSpacing: _itemPadding,
|
||||||
S.of(context).documentDetailsPageTabMetaDataLabel,
|
metaData: _metaData,
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
const SimilarDocumentsView(),
|
||||||
Tab(
|
],
|
||||||
child: Text(
|
),
|
||||||
S
|
);
|
||||||
.of(context)
|
},
|
||||||
.documentDetailsPageTabSimilarDocumentsLabel,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => SimilarDocumentsCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
documentId: state.document.id,
|
|
||||||
),
|
|
||||||
child: TabBarView(
|
|
||||||
children: [
|
|
||||||
DocumentOverviewWidget(
|
|
||||||
document: state.document,
|
|
||||||
itemSpacing: _itemPadding,
|
|
||||||
queryString: widget.titleAndContentQueryString,
|
|
||||||
),
|
|
||||||
DocumentContentWidget(
|
|
||||||
isFullContentLoaded: state.isFullContentLoaded,
|
|
||||||
document: state.document,
|
|
||||||
fullContent: state.fullContent,
|
|
||||||
queryString: widget.titleAndContentQueryString,
|
|
||||||
),
|
|
||||||
DocumentMetaDataWidget(
|
|
||||||
document: state.document,
|
|
||||||
itemSpacing: _itemPadding,
|
|
||||||
metaData: _metaData,
|
|
||||||
),
|
|
||||||
const SimilarDocumentsView(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,18 +25,17 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, state) {
|
builder: (context, connectivity) {
|
||||||
if (!state.isConnected) {
|
|
||||||
return const Center(
|
|
||||||
child: OfflineWidget(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return FutureBuilder<DocumentMetaData>(
|
return FutureBuilder<DocumentMetaData>(
|
||||||
future: metaData,
|
future: metaData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
if (!connectivity.isConnected && !snapshot.hasData) {
|
||||||
|
return OfflineWidget();
|
||||||
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
final meta = snapshot.data!;
|
final meta = snapshot.data!;
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -55,7 +54,9 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
|||||||
label: Text(S
|
label: Text(S
|
||||||
.of(context)
|
.of(context)
|
||||||
.documentDetailsPageAssignAsnButtonLabel),
|
.documentDetailsPageAssignAsnButtonLabel),
|
||||||
onPressed: () => _assignAsn(context),
|
onPressed: connectivity.isConnected
|
||||||
|
? () => _assignAsn(context)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
DetailsItem.text(DateFormat().format(document.modified),
|
DetailsItem.text(DateFormat().format(document.modified),
|
||||||
|
|||||||
@@ -36,11 +36,14 @@ class _DocumentViewState extends State<DocumentView> {
|
|||||||
),
|
),
|
||||||
body: PdfView(
|
body: PdfView(
|
||||||
builders: PdfViewBuilders<DefaultBuilderOptions>(
|
builders: PdfViewBuilders<DefaultBuilderOptions>(
|
||||||
options: const DefaultBuilderOptions(),
|
options: const DefaultBuilderOptions(
|
||||||
|
loaderSwitchDuration: Duration(milliseconds: 500),
|
||||||
|
),
|
||||||
pageLoaderBuilder: (context) => const Center(
|
pageLoaderBuilder: (context) => const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
controller: _pdfController,
|
controller: _pdfController,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_
|
|||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.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';
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart';
|
import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
||||||
@@ -42,10 +43,18 @@ class DocumentsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentsPageState extends State<DocumentsPage>
|
class _DocumentsPageState extends State<DocumentsPage>
|
||||||
with SingleTickerProviderStateMixin {
|
with
|
||||||
|
SingleTickerProviderStateMixin,
|
||||||
|
DocumentPagingViewMixin<DocumentsPage, DocumentsCubit> {
|
||||||
late final TabController _tabController;
|
late final TabController _tabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ScrollController get pagingScrollController =>
|
||||||
|
_nestedScrollViewKey.currentState?.innerController ?? ScrollController();
|
||||||
|
|
||||||
|
final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey();
|
||||||
int _currentTab = 0;
|
int _currentTab = 0;
|
||||||
|
bool _showBackToTopButton = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -53,21 +62,40 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
_tabController = TabController(
|
_tabController = TabController(
|
||||||
length: 2,
|
length: 2,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
initialIndex: 0,
|
|
||||||
);
|
);
|
||||||
try {
|
Future.wait([
|
||||||
context.read<DocumentsCubit>().reload();
|
context.read<DocumentsCubit>().reload(),
|
||||||
context.read<SavedViewCubit>().reload();
|
context.read<SavedViewCubit>().reload(),
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
]).onError<PaperlessServerException>(
|
||||||
showErrorMessage(context, error, stackTrace);
|
(error, stackTrace) {
|
||||||
}
|
showErrorMessage(context, error, stackTrace);
|
||||||
_tabController.addListener(_listenForTabChanges);
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_tabController.addListener(_tabChangesListener);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
_nestedScrollViewKey.currentState!.innerController
|
||||||
|
..addListener(_scrollExtentListener)
|
||||||
|
..addListener(shouldLoadMoreDocumentsListener);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenForTabChanges() {
|
void _tabChangesListener() {
|
||||||
setState(() {
|
setState(() => _currentTab = _tabController.index);
|
||||||
_currentTab = _tabController.index;
|
}
|
||||||
});
|
|
||||||
|
void _scrollExtentListener() {
|
||||||
|
if (pagingScrollController.position.pixels >
|
||||||
|
MediaQuery.of(context).size.height) {
|
||||||
|
if (!_showBackToTopButton) {
|
||||||
|
setState(() => _showBackToTopButton = true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_showBackToTopButton) {
|
||||||
|
setState(() => _showBackToTopButton = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -145,186 +173,100 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: NestedScrollView(
|
child: Stack(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
children: [
|
||||||
SliverOverlapAbsorber(
|
NestedScrollView(
|
||||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
key: _nestedScrollViewKey,
|
||||||
// and redirects it to the SliverOverlapInjector below. If it is
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
// missing, then it is possible for the nested "inner" scroll view
|
SliverOverlapAbsorber(
|
||||||
// below to end up under the SliverAppBar even when the inner
|
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||||
// scroll view thinks it has not been scrolled.
|
// and redirects it to the SliverOverlapInjector below. If it is
|
||||||
// This is not necessary if the "headerSliverBuilder" only builds
|
// missing, then it is possible for the nested "inner" scroll view
|
||||||
// widgets that do not overlap the next sliver.
|
// below to end up under the SliverAppBar even when the inner
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
// scroll view thinks it has not been scrolled.
|
||||||
context,
|
// This is not necessary if the "headerSliverBuilder" only builds
|
||||||
),
|
// widgets that do not overlap the next sliver.
|
||||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||||
builder: (context, state) {
|
context,
|
||||||
if (state.selection.isNotEmpty) {
|
),
|
||||||
return SliverAppBar(
|
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
floating: false,
|
builder: (context, state) {
|
||||||
pinned: true,
|
if (state.selection.isNotEmpty) {
|
||||||
leading: IconButton(
|
return SliverAppBar(
|
||||||
icon: const Icon(Icons.close),
|
floating: false,
|
||||||
onPressed: () => context
|
pinned: true,
|
||||||
.read<DocumentsCubit>()
|
leading: IconButton(
|
||||||
.resetSelection(),
|
icon: const Icon(Icons.close),
|
||||||
),
|
onPressed: () => context
|
||||||
title: Text(
|
.read<DocumentsCubit>()
|
||||||
"${state.selection.length} ${S.of(context).documentsSelectedText}",
|
.resetSelection(),
|
||||||
),
|
),
|
||||||
actions: [
|
title: Text(
|
||||||
IconButton(
|
"${state.selection.length} ${S.of(context).documentsSelectedText}",
|
||||||
icon: const Icon(Icons.delete),
|
),
|
||||||
onPressed: () => _onDelete(state),
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () => _onDelete(state),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SearchAppBar(
|
||||||
|
hintText:
|
||||||
|
S.of(context).documentSearchSearchDocuments,
|
||||||
|
onOpenSearch: showDocumentSearchPage,
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: S.of(context).documentsPageTitle),
|
||||||
|
Tab(text: S.of(context).savedViewsLabel),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
final metrics = notification.metrics;
|
||||||
|
if (metrics.maxScrollExtent == 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return SearchAppBar(
|
final desiredTab =
|
||||||
hintText: S.of(context).documentSearchSearchDocuments,
|
(metrics.pixels / metrics.maxScrollExtent).round();
|
||||||
onOpenSearch: showDocumentSearchPage,
|
if (metrics.axis == Axis.horizontal &&
|
||||||
bottom: TabBar(
|
_currentTab != desiredTab) {
|
||||||
controller: _tabController,
|
setState(() => _currentTab = desiredTab);
|
||||||
tabs: [
|
}
|
||||||
Tab(text: S.of(context).documentsPageTitle),
|
return false;
|
||||||
Tab(text: S.of(context).savedViewsLabel),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
child: TabBarView(
|
||||||
),
|
controller: _tabController,
|
||||||
],
|
children: [
|
||||||
body: NotificationListener<ScrollNotification>(
|
Builder(
|
||||||
onNotification: (notification) {
|
builder: (context) {
|
||||||
final metrics = notification.metrics;
|
return _buildDocumentsTab(
|
||||||
if (metrics.maxScrollExtent == 0) {
|
connectivityState,
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final desiredTab =
|
|
||||||
(metrics.pixels / metrics.maxScrollExtent).round();
|
|
||||||
if (metrics.axis == Axis.horizontal &&
|
|
||||||
_currentTab != desiredTab) {
|
|
||||||
setState(() => _currentTab = desiredTab);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
child: NotificationListener<ScrollMetricsNotification>(
|
|
||||||
onNotification: (notification) {
|
|
||||||
// Listen for scroll notifications to load new data.
|
|
||||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
|
||||||
final currState = context.read<DocumentsCubit>().state;
|
|
||||||
final max = notification.metrics.maxScrollExtent;
|
|
||||||
if (max == 0 ||
|
|
||||||
_currentTab != 0 ||
|
|
||||||
currState.isLoading ||
|
|
||||||
currState.isLastPageLoaded) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final offset = notification.metrics.pixels;
|
|
||||||
if (offset >= max * 0.7) {
|
|
||||||
context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.loadMore()
|
|
||||||
.onError<PaperlessServerException>(
|
|
||||||
(error, stackTrace) => showErrorMessage(
|
|
||||||
context,
|
context,
|
||||||
error,
|
);
|
||||||
stackTrace,
|
},
|
||||||
),
|
),
|
||||||
);
|
Builder(
|
||||||
}
|
builder: (context) {
|
||||||
return false;
|
return _buildSavedViewsTab(
|
||||||
},
|
connectivityState,
|
||||||
child: TabBarView(
|
context,
|
||||||
controller: _tabController,
|
);
|
||||||
children: [
|
},
|
||||||
Builder(
|
),
|
||||||
builder: (context) {
|
],
|
||||||
return RefreshIndicator(
|
),
|
||||||
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
|
||||||
onRefresh: _onReloadDocuments,
|
|
||||||
notificationPredicate: (_) =>
|
|
||||||
connectivityState.isConnected,
|
|
||||||
child: CustomScrollView(
|
|
||||||
key: const PageStorageKey<String>("documents"),
|
|
||||||
slivers: <Widget>[
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: NestedScrollView
|
|
||||||
.sliverOverlapAbsorberHandleFor(
|
|
||||||
context),
|
|
||||||
),
|
|
||||||
_buildViewActions(),
|
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.hasLoaded &&
|
|
||||||
state.documents.isEmpty) {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: DocumentsEmptyState(
|
|
||||||
state: state,
|
|
||||||
onReset: () {
|
|
||||||
context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.resetFilter();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SliverAdaptiveDocumentsView(
|
|
||||||
viewType: state.viewType,
|
|
||||||
onTap: _openDetails,
|
|
||||||
onSelected: context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.toggleDocumentSelection,
|
|
||||||
hasInternetConnection:
|
|
||||||
connectivityState.isConnected,
|
|
||||||
onTagSelected: _addTagToFilter,
|
|
||||||
onCorrespondentSelected:
|
|
||||||
_addCorrespondentToFilter,
|
|
||||||
onDocumentTypeSelected:
|
|
||||||
_addDocumentTypeToFilter,
|
|
||||||
onStoragePathSelected:
|
|
||||||
_addStoragePathToFilter,
|
|
||||||
documents: state.documents,
|
|
||||||
hasLoaded: state.hasLoaded,
|
|
||||||
isLabelClickable: true,
|
|
||||||
isLoading: state.isLoading,
|
|
||||||
selectedDocumentIds: state.selectedIds,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
return RefreshIndicator(
|
|
||||||
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
|
||||||
onRefresh: _onReloadSavedViews,
|
|
||||||
notificationPredicate: (_) =>
|
|
||||||
connectivityState.isConnected,
|
|
||||||
child: CustomScrollView(
|
|
||||||
key: const PageStorageKey<String>("savedViews"),
|
|
||||||
slivers: <Widget>[
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: NestedScrollView
|
|
||||||
.sliverOverlapAbsorberHandleFor(
|
|
||||||
context),
|
|
||||||
),
|
|
||||||
const SavedViewList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_showBackToTopButton) _buildBackToTopAction(context),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -333,6 +275,109 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBackToTopAction(BuildContext context) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: const Offset(24, -24),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: ActionChip(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
side: BorderSide.none,
|
||||||
|
avatar: Icon(
|
||||||
|
Icons.expand_less,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
await pagingScrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInExpo,
|
||||||
|
);
|
||||||
|
_nestedScrollViewKey.currentState?.outerController.jumpTo(0);
|
||||||
|
},
|
||||||
|
label: Text(
|
||||||
|
"Go to top", //TODO: INTL
|
||||||
|
style: DefaultTextStyle.of(context).style.apply(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSavedViewsTab(
|
||||||
|
ConnectivityState connectivityState,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||||
|
onRefresh: _onReloadSavedViews,
|
||||||
|
notificationPredicate: (_) => connectivityState.isConnected,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>("savedViews"),
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
|
),
|
||||||
|
const SavedViewList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDocumentsTab(
|
||||||
|
ConnectivityState connectivityState,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||||
|
onRefresh: _onReloadDocuments,
|
||||||
|
notificationPredicate: (_) => connectivityState.isConnected,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>("documents"),
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
|
),
|
||||||
|
_buildViewActions(),
|
||||||
|
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.hasLoaded && state.documents.isEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: DocumentsEmptyState(
|
||||||
|
state: state,
|
||||||
|
onReset: context.read<DocumentsCubit>().resetFilter,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverAdaptiveDocumentsView(
|
||||||
|
viewType: state.viewType,
|
||||||
|
onTap: _openDetails,
|
||||||
|
onSelected:
|
||||||
|
context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||||
|
hasInternetConnection: connectivityState.isConnected,
|
||||||
|
onTagSelected: _addTagToFilter,
|
||||||
|
onCorrespondentSelected: _addCorrespondentToFilter,
|
||||||
|
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
||||||
|
onStoragePathSelected: _addStoragePathToFilter,
|
||||||
|
documents: state.documents,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
isLabelClickable: true,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
selectedDocumentIds: state.selectedIds,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildViewActions() {
|
Widget _buildViewActions() {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_grid_loading_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
abstract class AdaptiveDocumentsView extends StatelessWidget {
|
abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||||
@@ -42,6 +43,24 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
required this.hasLoaded,
|
required this.hasLoaded,
|
||||||
this.enableHeroAnimation = true,
|
this.enableHeroAnimation = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AdaptiveDocumentsView.fromPagedState(
|
||||||
|
DocumentPagingState state, {
|
||||||
|
super.key,
|
||||||
|
this.onSelected,
|
||||||
|
this.onTap,
|
||||||
|
this.onCorrespondentSelected,
|
||||||
|
this.onDocumentTypeSelected,
|
||||||
|
this.onStoragePathSelected,
|
||||||
|
this.onTagSelected,
|
||||||
|
this.isLabelClickable = true,
|
||||||
|
this.enableHeroAnimation = true,
|
||||||
|
required this.hasInternetConnection,
|
||||||
|
this.viewType = ViewType.list,
|
||||||
|
this.selectedDocumentIds = const [],
|
||||||
|
}) : documents = state.documents,
|
||||||
|
isLoading = state.isLoading,
|
||||||
|
hasLoaded = state.hasLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||||
@@ -115,7 +134,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
(context, index) {
|
(context, index) {
|
||||||
final document = documents.elementAt(index);
|
final document = documents.elementAt(index);
|
||||||
return LabelRepositoriesProvider(
|
return LabelRepositoriesProvider(
|
||||||
child: DocumentGridItem(
|
child: DocumentDetailedItem(
|
||||||
isLabelClickable: isLabelClickable,
|
isLabelClickable: isLabelClickable,
|
||||||
document: document,
|
document: document,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -136,7 +155,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
|
|
||||||
Widget _buildGridView() {
|
Widget _buildGridView() {
|
||||||
if (showLoadingPlaceholder) {
|
if (showLoadingPlaceholder) {
|
||||||
return DocumentGridLoadingWidget.sliver();
|
return const DocumentGridLoadingWidget.sliver();
|
||||||
}
|
}
|
||||||
return SliverGrid.builder(
|
return SliverGrid.builder(
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
@@ -205,6 +224,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
primary: false,
|
primary: false,
|
||||||
itemCount: documents.length,
|
itemCount: documents.length,
|
||||||
@@ -235,6 +255,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
physics: const PageScrollPhysics(),
|
physics: const PageScrollPhysics(),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
primary: false,
|
primary: false,
|
||||||
@@ -265,6 +286,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
return DocumentGridLoadingWidget();
|
return DocumentGridLoadingWidget();
|
||||||
}
|
}
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
primary: false,
|
primary: false,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
|||||||
@@ -2,51 +2,59 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class DocumentPreview extends StatelessWidget {
|
class DocumentPreview extends StatelessWidget {
|
||||||
final int id;
|
final DocumentModel document;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final Alignment alignment;
|
final Alignment alignment;
|
||||||
final double borderRadius;
|
final double borderRadius;
|
||||||
final bool enableHero;
|
final bool enableHero;
|
||||||
|
final double scale;
|
||||||
|
|
||||||
const DocumentPreview({
|
const DocumentPreview({
|
||||||
super.key,
|
super.key,
|
||||||
required this.id,
|
required this.document,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.topCenter,
|
||||||
this.borderRadius = 8.0,
|
this.borderRadius = 12.0,
|
||||||
this.enableHero = true,
|
this.enableHero = true,
|
||||||
|
this.scale = 1.1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!enableHero) {
|
return HeroMode(
|
||||||
return _buildPreview(context);
|
enabled: enableHero,
|
||||||
}
|
child: Hero(
|
||||||
return Hero(
|
tag: "thumb_${document.id}",
|
||||||
tag: "thumb_$id",
|
child: _buildPreview(context),
|
||||||
child: _buildPreview(context),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipRRect _buildPreview(BuildContext context) {
|
Widget _buildPreview(BuildContext context) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
child: CachedNetworkImage(
|
child: Transform.scale(
|
||||||
fit: fit,
|
scale: scale,
|
||||||
alignment: Alignment.topCenter,
|
child: CachedNetworkImage(
|
||||||
cacheKey: "thumb_$id",
|
fit: fit,
|
||||||
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(id),
|
alignment: alignment,
|
||||||
errorWidget: (ctxt, msg, __) => Text(msg),
|
cacheKey: "thumb_${document.id}",
|
||||||
placeholder: (context, value) => Shimmer.fromColors(
|
imageUrl: context
|
||||||
baseColor: Colors.grey[300]!,
|
.read<PaperlessDocumentsApi>()
|
||||||
highlightColor: Colors.grey[100]!,
|
.getThumbnailUrl(document.id),
|
||||||
child: const SizedBox(height: 100, width: 100),
|
errorWidget: (ctxt, msg, __) => Text(msg),
|
||||||
|
placeholder: (context, value) => Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: const SizedBox(height: 100, width: 100),
|
||||||
|
),
|
||||||
|
cacheManager: context.watch<CacheManager>(),
|
||||||
),
|
),
|
||||||
cacheManager: context.watch<CacheManager>(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
class DocumentDetailedItem extends DocumentItem {
|
class DocumentDetailedItem extends DocumentItem {
|
||||||
const DocumentDetailedItem({
|
const DocumentDetailedItem({
|
||||||
@@ -20,12 +27,118 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
final insets = MediaQuery.of(context).viewInsets;
|
||||||
|
final padding = MediaQuery.of(context).viewPadding;
|
||||||
|
final availableHeight = size.height -
|
||||||
|
insets.top -
|
||||||
|
insets.bottom -
|
||||||
|
padding.top -
|
||||||
|
padding.bottom -
|
||||||
|
kBottomNavigationBarHeight -
|
||||||
|
kToolbarHeight;
|
||||||
|
final maxHeight = min(500.0, availableHeight);
|
||||||
return Card(
|
return Card(
|
||||||
child: Column(
|
child: InkWell(
|
||||||
children: [
|
enableFeedback: true,
|
||||||
DocumentPreview(id: document.id),
|
borderRadius: BorderRadius.circular(12),
|
||||||
],
|
onTap: () {
|
||||||
|
if (isSelectionActive) {
|
||||||
|
onSelected?.call(document);
|
||||||
|
} else {
|
||||||
|
onTap?.call(document);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
onSelected?.call(document);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
width: double.infinity,
|
||||||
|
height: maxHeight / 2,
|
||||||
|
),
|
||||||
|
child: DocumentPreview(
|
||||||
|
document: document,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat.yMMMMd().format(document.created),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.apply(color: Theme.of(context).hintColor),
|
||||||
|
),
|
||||||
|
if (document.archiveSerialNumber != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'#${document.archiveSerialNumber}',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.apply(color: Theme.of(context).hintColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddedLTRB(8, 8, 8, 4),
|
||||||
|
Text(
|
||||||
|
document.title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.person_outline,
|
||||||
|
size: 16,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
|
CorrespondentWidget(
|
||||||
|
onSelected: onCorrespondentSelected,
|
||||||
|
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
correspondentId: document.correspondent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.description_outlined,
|
||||||
|
size: 16,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
|
DocumentTypeWidget(
|
||||||
|
onSelected: onDocumentTypeSelected,
|
||||||
|
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
documentTypeId: document.documentType,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
|
TagsWidget(
|
||||||
|
isMultiLine: false,
|
||||||
|
tagIds: document.tags,
|
||||||
|
).padded(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
).padded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,66 +25,62 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return Padding(
|
||||||
onTap: _onTap,
|
padding: const EdgeInsets.all(8.0),
|
||||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
child: Card(
|
||||||
child: AbsorbPointer(
|
elevation: 1.0,
|
||||||
absorbing: isSelectionActive,
|
color: isSelected
|
||||||
child: Padding(
|
? Theme.of(context).colorScheme.inversePrimary
|
||||||
padding: const EdgeInsets.all(8.0),
|
: Theme.of(context).cardColor,
|
||||||
child: Card(
|
child: InkWell(
|
||||||
elevation: 1.0,
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: isSelected
|
onTap: _onTap,
|
||||||
? Theme.of(context).colorScheme.inversePrimary
|
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||||
: Theme.of(context).cardColor,
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
AspectRatio(
|
||||||
AspectRatio(
|
aspectRatio: 1,
|
||||||
aspectRatio: 1,
|
child: DocumentPreview(
|
||||||
child: DocumentPreview(
|
document: document,
|
||||||
id: document.id,
|
borderRadius: 12.0,
|
||||||
borderRadius: 12.0,
|
enableHero: enableHeroAnimation,
|
||||||
enableHero: enableHeroAnimation,
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CorrespondentWidget(
|
||||||
|
correspondentId: document.correspondent,
|
||||||
|
),
|
||||||
|
DocumentTypeWidget(
|
||||||
|
documentTypeId: document.documentType,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
document.title,
|
||||||
|
maxLines: document.tags.isEmpty ? 3 : 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
TagsWidget(
|
||||||
|
tagIds: document.tags,
|
||||||
|
isMultiLine: false,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
DateFormat.yMMMd().format(document.created),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
CorrespondentWidget(
|
|
||||||
correspondentId: document.correspondent,
|
|
||||||
),
|
|
||||||
DocumentTypeWidget(
|
|
||||||
documentTypeId: document.documentType,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
document.title,
|
|
||||||
maxLines: document.tags.isEmpty ? 3 : 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
TagsWidget(
|
|
||||||
tagIds: document.tags,
|
|
||||||
isMultiLine: false,
|
|
||||||
onTagSelected: onTagSelected,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
DateFormat.yMMMd().format(
|
|
||||||
document.created,
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class DocumentListItem extends DocumentItem {
|
|||||||
aspectRatio: _a4AspectRatio,
|
aspectRatio: _a4AspectRatio,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: DocumentPreview(
|
child: DocumentPreview(
|
||||||
id: document.id,
|
document: document,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
enableHero: enableHeroAnimation,
|
enableHero: enableHeroAnimation,
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
|
|
||||||
class DocumentGridLoadingWidget extends StatelessWidget
|
class DocumentGridLoadingWidget extends StatelessWidget {
|
||||||
with DocumentItemPlaceholder {
|
|
||||||
final bool _isSliver;
|
final bool _isSliver;
|
||||||
@override
|
@override
|
||||||
final Random random = Random(1257195195);
|
const DocumentGridLoadingWidget({super.key}) : _isSliver = false;
|
||||||
DocumentGridLoadingWidget({super.key}) : _isSliver = false;
|
|
||||||
|
|
||||||
DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
|
const DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -41,8 +32,6 @@ class DocumentGridLoadingWidget extends StatelessWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaceholderGridItem(BuildContext context) {
|
Widget _buildPlaceholderGridItem(BuildContext context) {
|
||||||
final values = nextValues;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -68,21 +57,25 @@ class DocumentGridLoadingWidget extends StatelessWidget
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextPlaceholder(
|
const TextPlaceholder(
|
||||||
length: values.correspondentLength,
|
length: 70,
|
||||||
|
fontSize: 16,
|
||||||
|
).padded(1),
|
||||||
|
const TextPlaceholder(
|
||||||
|
length: 50,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
).padded(1),
|
).padded(1),
|
||||||
TextPlaceholder(
|
TextPlaceholder(
|
||||||
length: values.titleLength,
|
length: 200,
|
||||||
fontSize: 16,
|
fontSize:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.fontSize ??
|
||||||
|
10,
|
||||||
|
).padded(1),
|
||||||
|
const Spacer(),
|
||||||
|
const TagsPlaceholder(
|
||||||
|
count: 2,
|
||||||
|
dense: true,
|
||||||
),
|
),
|
||||||
if (values.tagCount > 0) ...[
|
|
||||||
const Spacer(),
|
|
||||||
TagsPlaceholder(
|
|
||||||
count: values.tagCount,
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
TextPlaceholder(
|
TextPlaceholder(
|
||||||
length: 100,
|
length: 100,
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
mixin DocumentItemPlaceholder {
|
|
||||||
static const _tags = [" ", " ", " "];
|
|
||||||
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
|
||||||
static const _correspondentLengths = <double>[120.0, 80.0, 40.0];
|
|
||||||
|
|
||||||
Random get random;
|
|
||||||
|
|
||||||
RandomDocumentItemPlaceholderValues get nextValues {
|
|
||||||
return RandomDocumentItemPlaceholderValues(
|
|
||||||
tagCount: random.nextInt(_tags.length + 1),
|
|
||||||
correspondentLength: _correspondentLengths[
|
|
||||||
random.nextInt(_correspondentLengths.length - 1)],
|
|
||||||
titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RandomDocumentItemPlaceholderValues {
|
|
||||||
final int tagCount;
|
|
||||||
final double correspondentLength;
|
|
||||||
final double titleLength;
|
|
||||||
|
|
||||||
RandomDocumentItemPlaceholderValues({
|
|
||||||
required this.tagCount,
|
|
||||||
required this.correspondentLength,
|
|
||||||
required this.titleLength,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
||||||
|
|
||||||
class DocumentsListLoadingWidget extends StatelessWidget
|
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||||
with DocumentItemPlaceholder {
|
|
||||||
final bool _isSliver;
|
final bool _isSliver;
|
||||||
DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
const DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||||
|
|
||||||
DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
const DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||||
|
|
||||||
@override
|
|
||||||
final Random random = Random(1209571050);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -35,26 +27,31 @@ class DocumentsListLoadingWidget extends StatelessWidget
|
|||||||
|
|
||||||
Widget _buildFakeListItem(BuildContext context) {
|
Widget _buildFakeListItem(BuildContext context) {
|
||||||
const fontSize = 14.0;
|
const fontSize = 14.0;
|
||||||
final values = nextValues;
|
|
||||||
return ShimmerPlaceholder(
|
return ShimmerPlaceholder(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: const EdgeInsets.all(8),
|
contentPadding: const EdgeInsets.all(8),
|
||||||
dense: true,
|
dense: true,
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
width: 35,
|
width: 35,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Row(
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextPlaceholder(
|
const TextPlaceholder(
|
||||||
length: values.correspondentLength,
|
length: 120,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
TextPlaceholder(
|
||||||
|
length: 220,
|
||||||
|
fontSize: Theme.of(context).textTheme.titleMedium!.fontSize!,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
@@ -63,14 +60,10 @@ class DocumentsListLoadingWidget extends StatelessWidget
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
|
TagsPlaceholder(count: 2, dense: true),
|
||||||
|
SizedBox(height: 2),
|
||||||
TextPlaceholder(
|
TextPlaceholder(
|
||||||
length: values.titleLength,
|
length: 250,
|
||||||
fontSize: fontSize,
|
|
||||||
),
|
|
||||||
if (values.tagCount > 0)
|
|
||||||
TagsPlaceholder(count: values.tagCount, dense: true),
|
|
||||||
TextPlaceholder(
|
|
||||||
length: 100,
|
|
||||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,
|
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -15,6 +15,7 @@ class TagsPlaceholder extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: (context, index) => FilterChip(
|
itemBuilder: (context, index) => FilterChip(
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
|
|
||||||
class TextPlaceholder extends StatelessWidget {
|
class TextPlaceholder extends StatelessWidget {
|
||||||
final double length;
|
final double length;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
/// Meant to be used with blocbuilder.
|
/// Meant to be used with blocbuilder.
|
||||||
@@ -28,22 +27,26 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return PopupMenuButton<ViewType>(
|
return PopupMenuButton<ViewType>(
|
||||||
child: Icon(icon),
|
initialValue: viewType,
|
||||||
|
icon: Icon(icon),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
_buildViewTypeOption(
|
_buildViewTypeOption(
|
||||||
ViewType.list,
|
context,
|
||||||
'List',
|
type: ViewType.list,
|
||||||
Icons.list,
|
label: 'List', //TODO: INTL
|
||||||
|
icon: Icons.list,
|
||||||
),
|
),
|
||||||
_buildViewTypeOption(
|
_buildViewTypeOption(
|
||||||
ViewType.grid,
|
context,
|
||||||
'Grid',
|
type: ViewType.grid,
|
||||||
Icons.grid_view_rounded,
|
label: 'Grid', //TODO: INTL
|
||||||
|
icon: Icons.grid_view_rounded,
|
||||||
),
|
),
|
||||||
_buildViewTypeOption(
|
_buildViewTypeOption(
|
||||||
ViewType.detailed,
|
context,
|
||||||
'Detailed',
|
type: ViewType.detailed,
|
||||||
Icons.article_outlined,
|
label: 'Detailed', //TODO: INTL
|
||||||
|
icon: Icons.article_outlined,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (next) {
|
onSelected: (next) {
|
||||||
@@ -53,17 +56,22 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuItem<ViewType> _buildViewTypeOption(
|
PopupMenuItem<ViewType> _buildViewTypeOption(
|
||||||
ViewType type,
|
BuildContext context, {
|
||||||
String label,
|
required ViewType type,
|
||||||
IconData icon,
|
required String label,
|
||||||
) {
|
required IconData icon,
|
||||||
|
}) {
|
||||||
|
final selected = type == viewType;
|
||||||
return PopupMenuItem(
|
return PopupMenuItem(
|
||||||
value: type,
|
value: type,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
selected: type == viewType,
|
selected: selected,
|
||||||
trailing: type == viewType ? const Icon(Icons.done) : null,
|
trailing: selected ? const Icon(Icons.done) : null,
|
||||||
title: Text(label),
|
title: Text(label),
|
||||||
|
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||||
|
textColor: Theme.of(context).colorScheme.onSurface,
|
||||||
leading: Icon(icon),
|
leading: Icon(icon),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
|
|||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
@@ -63,7 +62,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
context.read(),
|
context.read(),
|
||||||
);
|
);
|
||||||
_listenToInboxChanges();
|
_listenToInboxChanges();
|
||||||
context.read<ConnectivityCubit>().reload();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
_listenForReceivedFiles();
|
_listenForReceivedFiles();
|
||||||
});
|
});
|
||||||
@@ -82,12 +81,22 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed && !_inboxTimer.isActive) {
|
switch (state) {
|
||||||
log('App is now in foreground, start polling for statistics.');
|
case AppLifecycleState.resumed:
|
||||||
_listenToInboxChanges();
|
log('App is now in foreground');
|
||||||
} else if (state != AppLifecycleState.resumed) {
|
context.read<ConnectivityCubit>().reload();
|
||||||
log('App is now in background, stop polling for statistics.');
|
log("Reloaded device connectivity state");
|
||||||
_inboxTimer.cancel();
|
if (!_inboxTimer.isActive) {
|
||||||
|
_listenToInboxChanges();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
default:
|
||||||
|
log('App is now in background');
|
||||||
|
_inboxTimer.cancel();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,21 +281,10 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
child: const LabelsPage(),
|
child: const LabelsPage(),
|
||||||
),
|
),
|
||||||
MultiBlocProvider(
|
BlocProvider<InboxCubit>.value(
|
||||||
providers: [
|
value: _inboxCubit,
|
||||||
// We need to manually downcast the inboxcubit to the
|
|
||||||
// mixed-in DocumentPagingBlocMixin to use the
|
|
||||||
// DocumentPagingViewMixin in the inbox.
|
|
||||||
BlocProvider<DocumentPagingBlocMixin>.value(
|
|
||||||
value: _inboxCubit,
|
|
||||||
),
|
|
||||||
BlocProvider<InboxCubit>.value(
|
|
||||||
value: _inboxCubit,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: const InboxPage(),
|
child: const InboxPage(),
|
||||||
),
|
),
|
||||||
// const SettingsPage(),
|
|
||||||
];
|
];
|
||||||
return MultiBlocListener(
|
return MultiBlocListener(
|
||||||
listeners: [
|
listeners: [
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.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/document_search_page.dart';
|
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart';
|
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
|
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
|
||||||
@@ -24,7 +24,8 @@ class InboxPage extends StatefulWidget {
|
|||||||
State<InboxPage> createState() => _InboxPageState();
|
State<InboxPage> createState() => _InboxPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InboxPageState extends State<InboxPage> with DocumentPagingViewMixin {
|
class _InboxPageState extends State<InboxPage>
|
||||||
|
with DocumentPagingViewMixin<InboxPage, InboxCubit> {
|
||||||
@override
|
@override
|
||||||
final pagingScrollController = ScrollController();
|
final pagingScrollController = ScrollController();
|
||||||
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class _InboxItemState extends State<InboxItem> {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: InboxItem._a4AspectRatio,
|
aspectRatio: InboxItem._a4AspectRatio,
|
||||||
child: DocumentPreview(
|
child: DocumentPreview(
|
||||||
id: widget.document.id,
|
document: widget.document,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
enableHero: false,
|
enableHero: false,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class DocumentTypeWidget extends StatelessWidget {
|
|||||||
state.labels[documentTypeId]?.toString() ?? "-",
|
state.labels[documentTypeId]?.toString() ?? "-",
|
||||||
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
|
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
|
||||||
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
|
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class LinkedDocumentsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
|
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
|
||||||
with DocumentPagingViewMixin {
|
with DocumentPagingViewMixin<LinkedDocumentsPage, LinkedDocumentsCubit> {
|
||||||
@override
|
@override
|
||||||
final pagingScrollController = ScrollController();
|
final pagingScrollController = ScrollController();
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ 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/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
|
||||||
|
|
||||||
mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> {
|
mixin DocumentPagingViewMixin<T extends StatefulWidget,
|
||||||
|
Bloc extends DocumentPagingBlocMixin> on State<T> {
|
||||||
ScrollController get pagingScrollController;
|
ScrollController get pagingScrollController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -20,7 +20,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentPagingBlocMixin get _bloc => context.read<DocumentPagingBlocMixin>();
|
DocumentPagingBlocMixin get _bloc => context.read<Bloc>();
|
||||||
|
|
||||||
void shouldLoadMoreDocumentsListener() async {
|
void shouldLoadMoreDocumentsListener() async {
|
||||||
if (shouldLoadMoreDocuments) {
|
if (shouldLoadMoreDocuments) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
||||||
@@ -12,50 +13,55 @@ class SavedViewList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final savedViewCubit = context.read<SavedViewCubit>();
|
final savedViewCubit = context.read<SavedViewCubit>();
|
||||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, state) {
|
builder: (context, connectivity) {
|
||||||
if (state.value.isEmpty) {
|
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
return SliverToBoxAdapter(
|
builder: (context, state) {
|
||||||
child: HintCard(
|
if (state.value.isEmpty) {
|
||||||
hintText: S.of(context).savedViewsEmptyStateText,
|
return SliverToBoxAdapter(
|
||||||
),
|
child: HintCard(
|
||||||
);
|
hintText: S.of(context).savedViewsEmptyStateText,
|
||||||
}
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
final view = state.value.values.elementAt(index);
|
|
||||||
return ListTile(
|
|
||||||
title: Text(view.name),
|
|
||||||
subtitle: Text(
|
|
||||||
S
|
|
||||||
.of(context)
|
|
||||||
.savedViewsFiltersSetCount(view.filterRules.length),
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
);
|
||||||
Navigator.of(context).push(
|
}
|
||||||
MaterialPageRoute(
|
return SliverList(
|
||||||
builder: (context) => MultiBlocProvider(
|
delegate: SliverChildBuilderDelegate(
|
||||||
providers: [
|
(context, index) {
|
||||||
BlocProvider(
|
final view = state.value.values.elementAt(index);
|
||||||
create: (context) => SavedViewDetailsCubit(
|
return ListTile(
|
||||||
context.read(),
|
enabled: connectivity.isConnected,
|
||||||
context.read(),
|
title: Text(view.name),
|
||||||
savedView: view,
|
subtitle: Text(
|
||||||
|
S
|
||||||
|
.of(context)
|
||||||
|
.savedViewsFiltersSetCount(view.filterRules.length),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SavedViewDetailsCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
savedView: view,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: SavedViewDetailsPage(
|
||||||
|
onDelete: savedViewCubit.remove,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
child: SavedViewDetailsPage(
|
|
||||||
onDelete: savedViewCubit.remove,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
childCount: state.value.length,
|
||||||
},
|
),
|
||||||
childCount: state.value.length,
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class SavedViewDetailsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
||||||
with DocumentPagingViewMixin {
|
with DocumentPagingViewMixin<SavedViewDetailsPage, SavedViewDetailsCubit> {
|
||||||
@override
|
@override
|
||||||
final pagingScrollController = ScrollController();
|
final pagingScrollController = ScrollController();
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
|||||||
onChanged: cubit.setViewType,
|
onChanged: cubit.setViewType,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||||
@@ -29,13 +32,13 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
snap: true,
|
snap: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
backgroundColor: widget.backgroundColor,
|
backgroundColor: widget.backgroundColor,
|
||||||
title: SearchBar(
|
title: SearchBar(
|
||||||
height: kToolbarHeight - 8,
|
height: kToolbarHeight - 12,
|
||||||
supportingText: widget.hintText,
|
supportingText: widget.hintText,
|
||||||
onTap: () => widget.onOpenSearch(context),
|
onTap: () => widget.onOpenSearch(context),
|
||||||
leadingIcon: IconButton(
|
leadingIcon: IconButton(
|
||||||
@@ -43,17 +46,22 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
onPressed: Scaffold.of(context).openDrawer,
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
),
|
),
|
||||||
trailingIcon: IconButton(
|
trailingIcon: IconButton(
|
||||||
icon: const CircleAvatar(
|
icon: BlocBuilder<PaperlessServerInformationCubit,
|
||||||
child: Text("A"),
|
PaperlessServerInformationState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return CircleAvatar(
|
||||||
|
child: Text(state.information?.userInitials ?? ''),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AccountSettingsDialog(),
|
builder: (context) => const AccountSettingsDialog(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddedOnly(top: 4, bottom: 4),
|
).paddedOnly(top: 8, bottom: 4),
|
||||||
bottom: widget.bottom,
|
bottom: widget.bottom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,7 @@ class AccountSettingsDialog extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
child: Text(state.information?.username
|
child: Text(state.information?.userInitials ?? ''),
|
||||||
?.toUpperCase()
|
|
||||||
.substring(0, 1) ??
|
|
||||||
''),
|
|
||||||
),
|
),
|
||||||
title: Text(state.information?.username ?? ''),
|
title: Text(state.information?.username ?? ''),
|
||||||
subtitle: Text(state.information?.host ?? ''),
|
subtitle: Text(state.information?.host ?? ''),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
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/widgets/offline_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
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';
|
||||||
@@ -17,7 +18,7 @@ class SimilarDocumentsView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||||
with DocumentPagingViewMixin {
|
with DocumentPagingViewMixin<SimilarDocumentsView, SimilarDocumentsCubit> {
|
||||||
@override
|
@override
|
||||||
final pagingScrollController = ScrollController();
|
final pagingScrollController = ScrollController();
|
||||||
|
|
||||||
@@ -33,44 +34,50 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, state) {
|
listenWhen: (previous, current) =>
|
||||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
|
!previous.isConnected && current.isConnected,
|
||||||
return DocumentsEmptyState(
|
listener: (context, state) =>
|
||||||
state: state,
|
context.read<SimilarDocumentsCubit>().initialize(),
|
||||||
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter(
|
builder: (context, connectivity) {
|
||||||
filter: DocumentFilter.initial.copyWith(
|
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||||
moreLike: () =>
|
builder: (context, state) {
|
||||||
context.read<SimilarDocumentsCubit>().documentId,
|
if (!connectivity.isConnected && !state.hasLoaded) {
|
||||||
),
|
return const OfflineWidget();
|
||||||
),
|
}
|
||||||
);
|
if (state.hasLoaded &&
|
||||||
}
|
!state.isLoading &&
|
||||||
|
state.documents.isEmpty) {
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return DocumentsEmptyState(
|
||||||
builder: (context, connectivity) {
|
state: state,
|
||||||
return CustomScrollView(
|
onReset: () => context
|
||||||
controller: pagingScrollController,
|
.read<SimilarDocumentsCubit>()
|
||||||
slivers: [
|
.updateFilter(
|
||||||
SliverAdaptiveDocumentsView(
|
filter: DocumentFilter.initial.copyWith(
|
||||||
documents: state.documents,
|
moreLike: () =>
|
||||||
hasInternetConnection: connectivity.isConnected,
|
context.read<SimilarDocumentsCubit>().documentId,
|
||||||
isLabelClickable: false,
|
|
||||||
isLoading: state.isLoading,
|
|
||||||
hasLoaded: state.hasLoaded,
|
|
||||||
enableHeroAnimation: false,
|
|
||||||
onTap: (document) {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
DocumentDetailsRoute.routeName,
|
|
||||||
arguments: DocumentDetailsRouteArguments(
|
|
||||||
document: document,
|
|
||||||
isLabelClickable: false,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
}
|
||||||
],
|
return DefaultAdaptiveDocumentsView(
|
||||||
|
scrollController: pagingScrollController,
|
||||||
|
documents: state.documents,
|
||||||
|
hasInternetConnection: connectivity.isConnected,
|
||||||
|
isLabelClickable: false,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
enableHeroAnimation: false,
|
||||||
|
onTap: (document) {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
DocumentDetailsRoute.routeName,
|
||||||
|
arguments: DocumentDetailsRouteArguments(
|
||||||
|
document: document,
|
||||||
|
isLabelClickable: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ class PaperlessServerInformationModel {
|
|||||||
final String? username;
|
final String? username;
|
||||||
final String? host;
|
final String? host;
|
||||||
|
|
||||||
|
String? get userInitials {
|
||||||
|
return username?.substring(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
PaperlessServerInformationModel({
|
PaperlessServerInformationModel({
|
||||||
this.host,
|
this.host,
|
||||||
this.username,
|
this.username,
|
||||||
|
|||||||
Reference in New Issue
Block a user