mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 15: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 'package:dio/dio.dart';
|
||||
@@ -18,7 +19,9 @@ class SessionManager {
|
||||
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//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.responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
|
||||
@@ -71,7 +74,9 @@ class SessionManager {
|
||||
}
|
||||
|
||||
if (authToken != null) {
|
||||
client.options.headers.addAll({'Authorization': 'Token $authToken'});
|
||||
client.options.headers.addAll({
|
||||
HttpHeaders.authorizationHeader: 'Token $authToken',
|
||||
});
|
||||
}
|
||||
|
||||
if (serverInformation != null) {
|
||||
@@ -82,7 +87,7 @@ class SessionManager {
|
||||
void resetSettings() {
|
||||
client.httpClientAdapter = IOHttpClientAdapter();
|
||||
client.options.baseUrl = '';
|
||||
client.options.headers.remove('Authorization');
|
||||
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||
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(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
Widget paddedOnly(
|
||||
{double top = 0.0,
|
||||
Widget paddedOnly({
|
||||
double top = 0.0,
|
||||
double bottom = 0.0,
|
||||
double left = 0.0,
|
||||
double right = 0.0}) {
|
||||
double right = 0.0,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: top,
|
||||
@@ -30,6 +34,13 @@ extension WidgetPadding on Widget {
|
||||
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> {
|
||||
|
||||
@@ -49,6 +49,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMetaData();
|
||||
}
|
||||
|
||||
void _loadMetaData() {
|
||||
_metaData = context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
|
||||
@@ -64,8 +68,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
},
|
||||
child: DefaultTabController(
|
||||
length: 4,
|
||||
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) {
|
||||
_loadMetaData();
|
||||
setState(() {});
|
||||
},
|
||||
child: Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
||||
bottomNavigationBar: _buildBottomAppBar(),
|
||||
body: NestedScrollView(
|
||||
@@ -78,7 +90,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
flexibleSpace:
|
||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) => DocumentPreview(
|
||||
id: state.document.id,
|
||||
document: state.document,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
@@ -170,6 +182,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,18 +25,17 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, state) {
|
||||
if (!state.isConnected) {
|
||||
return const Center(
|
||||
child: OfflineWidget(),
|
||||
);
|
||||
}
|
||||
builder: (context, connectivity) {
|
||||
return FutureBuilder<DocumentMetaData>(
|
||||
future: metaData,
|
||||
builder: (context, snapshot) {
|
||||
if (!connectivity.isConnected && !snapshot.hasData) {
|
||||
return OfflineWidget();
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final meta = snapshot.data!;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -55,7 +54,9 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
||||
label: Text(S
|
||||
.of(context)
|
||||
.documentDetailsPageAssignAsnButtonLabel),
|
||||
onPressed: () => _assignAsn(context),
|
||||
onPressed: connectivity.isConnected
|
||||
? () => _assignAsn(context)
|
||||
: null,
|
||||
),
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(DateFormat().format(document.modified),
|
||||
|
||||
@@ -36,11 +36,14 @@ class _DocumentViewState extends State<DocumentView> {
|
||||
),
|
||||
body: PdfView(
|
||||
builders: PdfViewBuilders<DefaultBuilderOptions>(
|
||||
options: const DefaultBuilderOptions(),
|
||||
options: const DefaultBuilderOptions(
|
||||
loaderSwitchDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
pageLoaderBuilder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
scrollDirection: Axis.vertical,
|
||||
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/sort_documents_button.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/view/add_saved_view_page.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>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with
|
||||
SingleTickerProviderStateMixin,
|
||||
DocumentPagingViewMixin<DocumentsPage, DocumentsCubit> {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
ScrollController get pagingScrollController =>
|
||||
_nestedScrollViewKey.currentState?.innerController ?? ScrollController();
|
||||
|
||||
final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey();
|
||||
int _currentTab = 0;
|
||||
bool _showBackToTopButton = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -53,21 +62,40 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
_tabController = TabController(
|
||||
length: 2,
|
||||
vsync: this,
|
||||
initialIndex: 0,
|
||||
);
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
context.read<SavedViewCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
Future.wait([
|
||||
context.read<DocumentsCubit>().reload(),
|
||||
context.read<SavedViewCubit>().reload(),
|
||||
]).onError<PaperlessServerException>(
|
||||
(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() {
|
||||
setState(() {
|
||||
_currentTab = _tabController.index;
|
||||
});
|
||||
void _tabChangesListener() {
|
||||
setState(() => _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
|
||||
@@ -145,7 +173,10 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: NestedScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
NestedScrollView(
|
||||
key: _nestedScrollViewKey,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||
@@ -182,7 +213,8 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
}
|
||||
return SearchAppBar(
|
||||
hintText: S.of(context).documentSearchSearchDocuments,
|
||||
hintText:
|
||||
S.of(context).documentSearchSearchDocuments,
|
||||
onOpenSearch: showDocumentSearchPage,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
@@ -210,64 +242,115 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
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,
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildSavedViewsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_showBackToTopButton) _buildBackToTopAction(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: CustomScrollView(
|
||||
key: const PageStorageKey<String>("documents"),
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(
|
||||
context),
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
_buildViewActions(),
|
||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.hasLoaded &&
|
||||
state.documents.isEmpty) {
|
||||
if (state.hasLoaded && state.documents.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () {
|
||||
context
|
||||
.read<DocumentsCubit>()
|
||||
.resetFilter();
|
||||
},
|
||||
onReset: context.read<DocumentsCubit>().resetFilter,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -275,18 +358,13 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
return SliverAdaptiveDocumentsView(
|
||||
viewType: state.viewType,
|
||||
onTap: _openDetails,
|
||||
onSelected: context
|
||||
.read<DocumentsCubit>()
|
||||
.toggleDocumentSelection,
|
||||
hasInternetConnection:
|
||||
connectivityState.isConnected,
|
||||
onSelected:
|
||||
context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||
hasInternetConnection: connectivityState.isConnected,
|
||||
onTagSelected: _addTagToFilter,
|
||||
onCorrespondentSelected:
|
||||
_addCorrespondentToFilter,
|
||||
onDocumentTypeSelected:
|
||||
_addDocumentTypeToFilter,
|
||||
onStoragePathSelected:
|
||||
_addStoragePathToFilter,
|
||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
||||
onStoragePathSelected: _addStoragePathToFilter,
|
||||
documents: state.documents,
|
||||
hasLoaded: state.hasLoaded,
|
||||
isLabelClickable: true,
|
||||
@@ -298,39 +376,6 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildViewActions() {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.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/documents_list_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/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_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';
|
||||
|
||||
abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||
@@ -42,6 +43,24 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||
required this.hasLoaded,
|
||||
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 {
|
||||
@@ -115,7 +134,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
(context, index) {
|
||||
final document = documents.elementAt(index);
|
||||
return LabelRepositoriesProvider(
|
||||
child: DocumentGridItem(
|
||||
child: DocumentDetailedItem(
|
||||
isLabelClickable: isLabelClickable,
|
||||
document: document,
|
||||
onTap: onTap,
|
||||
@@ -136,7 +155,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
|
||||
Widget _buildGridView() {
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentGridLoadingWidget.sliver();
|
||||
return const DocumentGridLoadingWidget.sliver();
|
||||
}
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
@@ -205,6 +224,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
controller: scrollController,
|
||||
primary: false,
|
||||
itemCount: documents.length,
|
||||
@@ -235,6 +255,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const PageScrollPhysics(),
|
||||
controller: scrollController,
|
||||
primary: false,
|
||||
@@ -265,6 +286,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
return DocumentGridLoadingWidget();
|
||||
}
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
controller: scrollController,
|
||||
primary: false,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
|
||||
@@ -2,44 +2,51 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.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:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentPreview extends StatelessWidget {
|
||||
final int id;
|
||||
final DocumentModel document;
|
||||
final BoxFit fit;
|
||||
final Alignment alignment;
|
||||
final double borderRadius;
|
||||
final bool enableHero;
|
||||
final double scale;
|
||||
|
||||
const DocumentPreview({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.document,
|
||||
this.fit = BoxFit.cover,
|
||||
this.alignment = Alignment.center,
|
||||
this.borderRadius = 8.0,
|
||||
this.alignment = Alignment.topCenter,
|
||||
this.borderRadius = 12.0,
|
||||
this.enableHero = true,
|
||||
this.scale = 1.1,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!enableHero) {
|
||||
return _buildPreview(context);
|
||||
}
|
||||
return Hero(
|
||||
tag: "thumb_$id",
|
||||
return HeroMode(
|
||||
enabled: enableHero,
|
||||
child: Hero(
|
||||
tag: "thumb_${document.id}",
|
||||
child: _buildPreview(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ClipRRect _buildPreview(BuildContext context) {
|
||||
Widget _buildPreview(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: CachedNetworkImage(
|
||||
fit: fit,
|
||||
alignment: Alignment.topCenter,
|
||||
cacheKey: "thumb_$id",
|
||||
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(id),
|
||||
alignment: alignment,
|
||||
cacheKey: "thumb_${document.id}",
|
||||
imageUrl: context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.getThumbnailUrl(document.id),
|
||||
errorWidget: (ctxt, msg, __) => Text(msg),
|
||||
placeholder: (context, value) => Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
@@ -48,6 +55,7 @@ class DocumentPreview extends StatelessWidget {
|
||||
),
|
||||
cacheManager: context.watch<CacheManager>(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import 'dart:math';
|
||||
|
||||
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/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 {
|
||||
const DocumentDetailedItem({
|
||||
@@ -20,12 +27,118 @@ class DocumentDetailedItem extends DocumentItem {
|
||||
|
||||
@override
|
||||
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(
|
||||
child: Column(
|
||||
child: InkWell(
|
||||
enableFeedback: true,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
if (isSelectionActive) {
|
||||
onSelected?.call(document);
|
||||
} else {
|
||||
onTap?.call(document);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
onSelected?.call(document);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
DocumentPreview(id: document.id),
|
||||
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,25 +25,24 @@ class DocumentGridItem extends DocumentItem {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
child: AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
child: Padding(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 1.0,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _onTap,
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
document: document,
|
||||
borderRadius: 12.0,
|
||||
enableHero: enableHeroAnimation,
|
||||
),
|
||||
@@ -74,9 +73,7 @@ class DocumentGridItem extends DocumentItem {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
DateFormat.yMMMd().format(
|
||||
document.created,
|
||||
),
|
||||
DateFormat.yMMMd().format(document.created),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
@@ -87,7 +84,6 @@ class DocumentGridItem extends DocumentItem {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class DocumentListItem extends DocumentItem {
|
||||
aspectRatio: _a4AspectRatio,
|
||||
child: GestureDetector(
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
document: document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: enableHeroAnimation,
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.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/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/text_placeholder.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentGridLoadingWidget extends StatelessWidget
|
||||
with DocumentItemPlaceholder {
|
||||
class DocumentGridLoadingWidget extends StatelessWidget {
|
||||
final bool _isSliver;
|
||||
@override
|
||||
final Random random = Random(1257195195);
|
||||
DocumentGridLoadingWidget({super.key}) : _isSliver = false;
|
||||
const DocumentGridLoadingWidget({super.key}) : _isSliver = false;
|
||||
|
||||
DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
const DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -41,8 +32,6 @@ class DocumentGridLoadingWidget extends StatelessWidget
|
||||
}
|
||||
|
||||
Widget _buildPlaceholderGridItem(BuildContext context) {
|
||||
final values = nextValues;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
@@ -68,21 +57,25 @@ class DocumentGridLoadingWidget extends StatelessWidget
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextPlaceholder(
|
||||
length: values.correspondentLength,
|
||||
const TextPlaceholder(
|
||||
length: 70,
|
||||
fontSize: 16,
|
||||
).padded(1),
|
||||
const TextPlaceholder(
|
||||
length: 50,
|
||||
fontSize: 16,
|
||||
).padded(1),
|
||||
TextPlaceholder(
|
||||
length: values.titleLength,
|
||||
fontSize: 16,
|
||||
),
|
||||
if (values.tagCount > 0) ...[
|
||||
length: 200,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleMedium?.fontSize ??
|
||||
10,
|
||||
).padded(1),
|
||||
const Spacer(),
|
||||
TagsPlaceholder(
|
||||
count: values.tagCount,
|
||||
const TagsPlaceholder(
|
||||
count: 2,
|
||||
dense: true,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
TextPlaceholder(
|
||||
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: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/text_placeholder.dart';
|
||||
|
||||
class DocumentsListLoadingWidget extends StatelessWidget
|
||||
with DocumentItemPlaceholder {
|
||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
final bool _isSliver;
|
||||
DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||
const DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||
|
||||
DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
final Random random = Random(1209571050);
|
||||
const DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -35,26 +27,31 @@ class DocumentsListLoadingWidget extends StatelessWidget
|
||||
|
||||
Widget _buildFakeListItem(BuildContext context) {
|
||||
const fontSize = 14.0;
|
||||
final values = nextValues;
|
||||
return ShimmerPlaceholder(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
dense: true,
|
||||
isThreeLine: true,
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: double.infinity,
|
||||
width: 35,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextPlaceholder(
|
||||
length: values.correspondentLength,
|
||||
const TextPlaceholder(
|
||||
length: 120,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
TextPlaceholder(
|
||||
length: 220,
|
||||
fontSize: Theme.of(context).textTheme.titleMedium!.fontSize!,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
@@ -63,14 +60,10 @@ class DocumentsListLoadingWidget extends StatelessWidget
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
TagsPlaceholder(count: 2, dense: true),
|
||||
SizedBox(height: 2),
|
||||
TextPlaceholder(
|
||||
length: values.titleLength,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
if (values.tagCount > 0)
|
||||
TagsPlaceholder(count: values.tagCount, dense: true),
|
||||
TextPlaceholder(
|
||||
length: 100,
|
||||
length: 250,
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,
|
||||
),
|
||||
],
|
||||
@@ -15,6 +15,7 @@ class TagsPlaceholder extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: count,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => FilterChip(
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
|
||||
class TextPlaceholder extends StatelessWidget {
|
||||
final double length;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
|
||||
/// Meant to be used with blocbuilder.
|
||||
@@ -28,22 +27,26 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
return PopupMenuButton<ViewType>(
|
||||
child: Icon(icon),
|
||||
initialValue: viewType,
|
||||
icon: Icon(icon),
|
||||
itemBuilder: (context) => [
|
||||
_buildViewTypeOption(
|
||||
ViewType.list,
|
||||
'List',
|
||||
Icons.list,
|
||||
context,
|
||||
type: ViewType.list,
|
||||
label: 'List', //TODO: INTL
|
||||
icon: Icons.list,
|
||||
),
|
||||
_buildViewTypeOption(
|
||||
ViewType.grid,
|
||||
'Grid',
|
||||
Icons.grid_view_rounded,
|
||||
context,
|
||||
type: ViewType.grid,
|
||||
label: 'Grid', //TODO: INTL
|
||||
icon: Icons.grid_view_rounded,
|
||||
),
|
||||
_buildViewTypeOption(
|
||||
ViewType.detailed,
|
||||
'Detailed',
|
||||
Icons.article_outlined,
|
||||
context,
|
||||
type: ViewType.detailed,
|
||||
label: 'Detailed', //TODO: INTL
|
||||
icon: Icons.article_outlined,
|
||||
),
|
||||
],
|
||||
onSelected: (next) {
|
||||
@@ -53,17 +56,22 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
PopupMenuItem<ViewType> _buildViewTypeOption(
|
||||
ViewType type,
|
||||
String label,
|
||||
IconData icon,
|
||||
) {
|
||||
BuildContext context, {
|
||||
required ViewType type,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
}) {
|
||||
final selected = type == viewType;
|
||||
return PopupMenuItem(
|
||||
value: type,
|
||||
child: ListTile(
|
||||
selected: type == viewType,
|
||||
trailing: type == viewType ? const Icon(Icons.done) : null,
|
||||
selected: selected,
|
||||
trailing: selected ? const Icon(Icons.done) : null,
|
||||
title: Text(label),
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
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/view/pages/labels_page.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/sharing/share_intent_queue.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(),
|
||||
);
|
||||
_listenToInboxChanges();
|
||||
context.read<ConnectivityCubit>().reload();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_listenForReceivedFiles();
|
||||
});
|
||||
@@ -82,12 +81,22 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && !_inboxTimer.isActive) {
|
||||
log('App is now in foreground, start polling for statistics.');
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
log('App is now in foreground');
|
||||
context.read<ConnectivityCubit>().reload();
|
||||
log("Reloaded device connectivity state");
|
||||
if (!_inboxTimer.isActive) {
|
||||
_listenToInboxChanges();
|
||||
} else if (state != AppLifecycleState.resumed) {
|
||||
log('App is now in background, stop polling for statistics.');
|
||||
}
|
||||
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(),
|
||||
),
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
// 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(),
|
||||
),
|
||||
// const SettingsPage(),
|
||||
];
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
|
||||
@@ -5,10 +5,10 @@ import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.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/documents/view/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_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/view/widgets/inbox_empty_widget.dart';
|
||||
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
|
||||
@@ -24,7 +24,8 @@ class InboxPage extends StatefulWidget {
|
||||
State<InboxPage> createState() => _InboxPageState();
|
||||
}
|
||||
|
||||
class _InboxPageState extends State<InboxPage> with DocumentPagingViewMixin {
|
||||
class _InboxPageState extends State<InboxPage>
|
||||
with DocumentPagingViewMixin<InboxPage, InboxCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@@ -54,7 +54,7 @@ class _InboxItemState extends State<InboxItem> {
|
||||
AspectRatio(
|
||||
aspectRatio: InboxItem._a4AspectRatio,
|
||||
child: DocumentPreview(
|
||||
id: widget.document.id,
|
||||
document: widget.document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: false,
|
||||
|
||||
@@ -31,6 +31,8 @@ class DocumentTypeWidget extends StatelessWidget {
|
||||
state.labels[documentTypeId]?.toString() ?? "-",
|
||||
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
|
||||
?.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>
|
||||
with DocumentPagingViewMixin {
|
||||
with DocumentPagingViewMixin<LinkedDocumentsPage, LinkedDocumentsCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.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/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;
|
||||
|
||||
@override
|
||||
@@ -20,7 +20,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
DocumentPagingBlocMixin get _bloc => context.read<DocumentPagingBlocMixin>();
|
||||
DocumentPagingBlocMixin get _bloc => context.read<Bloc>();
|
||||
|
||||
void shouldLoadMoreDocumentsListener() async {
|
||||
if (shouldLoadMoreDocuments) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.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/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
||||
@@ -12,6 +13,8 @@ class SavedViewList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final savedViewCubit = context.read<SavedViewCubit>();
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivity) {
|
||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
if (state.value.isEmpty) {
|
||||
@@ -26,6 +29,7 @@ class SavedViewList extends StatelessWidget {
|
||||
(context, index) {
|
||||
final view = state.value.values.elementAt(index);
|
||||
return ListTile(
|
||||
enabled: connectivity.isConnected,
|
||||
title: Text(view.name),
|
||||
subtitle: Text(
|
||||
S
|
||||
@@ -59,5 +63,7 @@ class SavedViewList extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class SavedViewDetailsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
||||
with DocumentPagingViewMixin {
|
||||
with DocumentPagingViewMixin<SavedViewDetailsPage, SavedViewDetailsCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
|
||||
@@ -56,7 +56,7 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
||||
onChanged: cubit.setViewType,
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||
@@ -29,13 +32,13 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
snap: true,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
title: SearchBar(
|
||||
height: kToolbarHeight - 8,
|
||||
height: kToolbarHeight - 12,
|
||||
supportingText: widget.hintText,
|
||||
onTap: () => widget.onOpenSearch(context),
|
||||
leadingIcon: IconButton(
|
||||
@@ -43,17 +46,22 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
||||
onPressed: Scaffold.of(context).openDrawer,
|
||||
),
|
||||
trailingIcon: IconButton(
|
||||
icon: const CircleAvatar(
|
||||
child: Text("A"),
|
||||
icon: BlocBuilder<PaperlessServerInformationCubit,
|
||||
PaperlessServerInformationState>(
|
||||
builder: (context, state) {
|
||||
return CircleAvatar(
|
||||
child: Text(state.information?.userInitials ?? ''),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AccountSettingsDialog(),
|
||||
builder: (context) => const AccountSettingsDialog(),
|
||||
);
|
||||
},
|
||||
),
|
||||
).paddedOnly(top: 4, bottom: 4),
|
||||
).paddedOnly(top: 8, bottom: 4),
|
||||
bottom: widget.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,10 +35,7 @@ class AccountSettingsDialog extends StatelessWidget {
|
||||
children: [
|
||||
ExpansionTile(
|
||||
leading: CircleAvatar(
|
||||
child: Text(state.information?.username
|
||||
?.toUpperCase()
|
||||
.substring(0, 1) ??
|
||||
''),
|
||||
child: Text(state.information?.userInitials ?? ''),
|
||||
),
|
||||
title: Text(state.information?.username ?? ''),
|
||||
subtitle: Text(state.information?.host ?? ''),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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/documents_empty_state.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>
|
||||
with DocumentPagingViewMixin {
|
||||
with DocumentPagingViewMixin<SimilarDocumentsView, SimilarDocumentsCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
|
||||
@@ -33,12 +34,25 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) =>
|
||||
context.read<SimilarDocumentsCubit>().initialize(),
|
||||
builder: (context, connectivity) {
|
||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
|
||||
if (!connectivity.isConnected && !state.hasLoaded) {
|
||||
return const OfflineWidget();
|
||||
}
|
||||
if (state.hasLoaded &&
|
||||
!state.isLoading &&
|
||||
state.documents.isEmpty) {
|
||||
return DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter(
|
||||
onReset: () => context
|
||||
.read<SimilarDocumentsCubit>()
|
||||
.updateFilter(
|
||||
filter: DocumentFilter.initial.copyWith(
|
||||
moreLike: () =>
|
||||
context.read<SimilarDocumentsCubit>().documentId,
|
||||
@@ -46,13 +60,8 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivity) {
|
||||
return CustomScrollView(
|
||||
controller: pagingScrollController,
|
||||
slivers: [
|
||||
SliverAdaptiveDocumentsView(
|
||||
return DefaultAdaptiveDocumentsView(
|
||||
scrollController: pagingScrollController,
|
||||
documents: state.documents,
|
||||
hasInternetConnection: connectivity.isConnected,
|
||||
isLabelClickable: false,
|
||||
@@ -69,8 +78,6 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -9,6 +9,10 @@ class PaperlessServerInformationModel {
|
||||
final String? username;
|
||||
final String? host;
|
||||
|
||||
String? get userInitials {
|
||||
return username?.substring(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
PaperlessServerInformationModel({
|
||||
this.host,
|
||||
this.username,
|
||||
|
||||
Reference in New Issue
Block a user