feat: Implemented new view type, fix connectivity issues, fix offline issues, fix loading animations, fix documents page paging

This commit is contained in:
Anton Stubenbord
2023-02-14 00:24:14 +01:00
parent c5033792aa
commit 6f66bf27fd
29 changed files with 806 additions and 606 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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