Merge pull request #99 from astubenbord/feature/detailed-view-type

Feature: Detailed view type
This commit is contained in:
Anton Stubenbord
2023-02-14 23:56:14 +01:00
committed by GitHub
44 changed files with 1028 additions and 726 deletions
+4 -9
View File
@@ -75,22 +75,17 @@ To get a local copy up and running follow these simple steps.
### Prerequisites ### Prerequisites
* Install an IDE of your choice (e.g. VSCode with the Dart/Flutter extensions) * Install an IDE of your choice (e.g. VSCode with the Dart/Flutter extensions)
* Install the flutter SDK (https://docs.flutter.dev/get-started/install) _or_ use the flutter git submodule pinned in this project by running `git submodule update --init` inside the project root directory.
*
### Install dependencies and generate files ### Install dependencies and generate files
1. First, clone the repository: 1. First, clone the repository:
```sh ```sh
git clone https://github.com/astubenbord/paperless-mobile.git git clone https://github.com/astubenbord/paperless-mobile.git
``` ```
In this project, flutter is pinned at a specific version as a git submodule to ensure all contributors work with the same environment and build with the same flutter version. You can also use your local flutter installation, just make sure that the app also compiles with the same flutter version as pinned in the `flutter` submodule when opening a pull request.
To download the pinned flutter SDK from the submodule and plan to install the dependencies manually in the next step, simply run You can now run the `scripts/install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate files for both the app and local packages.
```sh
git submodule update --init
```
You can now run the `scripts/install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate files for both the app and subpackages. Note that the `install_dependencies.sh` script will pull the flutter submodule and use the SDK to execute the flutter commands. If you want to manually install dependencies and build generated files, you can also run the following commands:
If you don't want to use submodules, you can also run the following commands using your local flutter installation:
#### Inside the `packages/paperless_api/` folder: #### Inside the `packages/paperless_api/` folder:
2. Install the dependencies for `paperless_api` 2. Install the dependencies for `paperless_api`
@@ -22,7 +22,7 @@ class DioHttpErrorInterceptor extends Interceptor {
DioError( DioError(
error: const PaperlessServerException(ErrorCode.deviceOffline), error: const PaperlessServerException(ErrorCode.deviceOffline),
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
type: DioErrorType.connectTimeout, type: DioErrorType.connectionTimeout,
), ),
); );
} }
@@ -52,7 +52,7 @@ class DioHttpErrorInterceptor extends Interceptor {
DioError( DioError(
error: errorMessages, error: errorMessages,
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
type: DioErrorType.response, type: DioErrorType.badResponse,
), ),
); );
} }
@@ -66,7 +66,7 @@ class DioHttpErrorInterceptor extends Interceptor {
handler.reject( handler.reject(
DioError( DioError(
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
type: DioErrorType.response, type: DioErrorType.badResponse,
error: const PaperlessServerException( error: const PaperlessServerException(
ErrorCode.missingClientCertificate), ErrorCode.missingClientCertificate),
), ),
@@ -28,10 +28,12 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
} }
bool _shouldRetryOnHttpException(DioError err) { bool _shouldRetryOnHttpException(DioError err) {
return err.type == DioErrorType.other && return err.type == DioErrorType.unknown &&
((err.error is HttpException && (err.error is HttpException &&
err.message.contains( (err.message?.contains(
'Connection closed before full header was received'))); 'Connection closed before full header was received',
) ??
false));
} }
} }
@@ -19,7 +19,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
); );
} }
} }
if (err.type == DioErrorType.connectTimeout) { if (err.type == DioErrorType.connectionTimeout) {
return _rejectWithStatus( return _rejectWithStatus(
ReachabilityStatus.connectionTimeout, ReachabilityStatus.connectionTimeout,
err, err,
@@ -55,6 +55,6 @@ void _rejectWithStatus(
error: reachabilityStatus, error: reachabilityStatus,
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
response: err.response, response: err.response,
type: DioErrorType.other, type: DioErrorType.unknown,
)); ));
} }
+13 -9
View File
@@ -1,9 +1,9 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart';
@@ -19,10 +19,12 @@ 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(
dio.options.receiveTimeout = const Duration(seconds: 25).inMilliseconds; BaseOptions(contentType: Headers.jsonContentType),
);
dio.options.receiveTimeout = const Duration(seconds: 25);
dio.options.responseType = ResponseType.json; dio.options.responseType = ResponseType.json;
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
(client) => client..badCertificateCallback = (cert, host, port) => true; (client) => client..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([ dio.interceptors.addAll([
...interceptors, ...interceptors,
@@ -59,7 +61,7 @@ class SessionManager {
clientCertificate.bytes, clientCertificate.bytes,
password: clientCertificate.passphrase, password: clientCertificate.passphrase,
); );
final adapter = DefaultHttpClientAdapter() final adapter = IOHttpClientAdapter()
..onHttpClientCreate = (client) => HttpClient(context: context) ..onHttpClientCreate = (client) => HttpClient(context: context)
..badCertificateCallback = ..badCertificateCallback =
(X509Certificate cert, String host, int port) => true; (X509Certificate cert, String host, int port) => true;
@@ -72,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) {
@@ -81,9 +85,9 @@ class SessionManager {
} }
void resetSettings() { void resetSettings() {
client.httpClientAdapter = DefaultHttpClientAdapter(); client.httpClientAdapter = IOHttpClientAdapter();
client.options.baseUrl = ''; client.options.baseUrl = '';
client.options.headers.remove('Authorization'); client.options.headers.remove(HttpHeaders.authorizationHeader);
serverInformation = PaperlessServerInformationModel(); serverInformation = PaperlessServerInformationModel();
} }
} }
@@ -1,8 +1,6 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:paperless_mobile/core/global/os_error_codes.dart'; import 'package:paperless_mobile/core/global/os_error_codes.dart';
import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart';
@@ -71,8 +69,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
SessionManager manager = SessionManager manager =
SessionManager([ServerReachabilityErrorInterceptor()]) SessionManager([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate) ..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = 5000 ..client.options.connectTimeout = const Duration(seconds: 5)
..client.options.receiveTimeout = 5000; ..client.options.receiveTimeout = const Duration(seconds: 5);
final response = await manager.client.get('$serverAddress/api/'); final response = await manager.client.get('$serverAddress/api/');
if (response.statusCode == 200) { if (response.statusCode == 200) {
@@ -80,7 +78,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
} }
return ReachabilityStatus.notReachable; return ReachabilityStatus.notReachable;
} on DioError catch (error) { } on DioError catch (error) {
if (error.type == DioErrorType.other && if (error.type == DioErrorType.unknown &&
error.error is ReachabilityStatus) { error.error is ReachabilityStatus) {
return error.error as ReachabilityStatus; return error.error as ReachabilityStatus;
} }
+17 -6
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> {
@@ -49,6 +49,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadMetaData();
}
void _loadMetaData() {
_metaData = context _metaData = context
.read<PaperlessDocumentsApi>() .read<PaperlessDocumentsApi>()
.getMetaData(context.read<DocumentDetailsCubit>().state.document); .getMetaData(context.read<DocumentDetailsCubit>().state.document);
@@ -64,108 +68,117 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}, },
child: DefaultTabController( child: DefaultTabController(
length: 4, length: 4,
child: Scaffold( child: BlocListener<ConnectivityCubit, ConnectivityState>(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, listenWhen: (previous, current) =>
floatingActionButton: widget.allowEdit ? _buildEditButton() : null, !previous.isConnected && current.isConnected,
bottomNavigationBar: _buildBottomAppBar(), listener: (context, state) {
body: NestedScrollView( _loadMetaData();
headerSliverBuilder: (context, innerBoxIsScrolled) => [ setState(() {});
SliverAppBar( },
leading: const BackButton(), child: Scaffold(
floating: true, floatingActionButtonLocation:
pinned: true, FloatingActionButtonLocation.endDocked,
expandedHeight: 200.0, floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
flexibleSpace: bottomNavigationBar: _buildBottomAppBar(),
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( body: NestedScrollView(
builder: (context, state) => DocumentPreview( headerSliverBuilder: (context, innerBoxIsScrolled) => [
id: state.document.id, SliverAppBar(
fit: BoxFit.cover, leading: const BackButton(),
floating: true,
pinned: true,
expandedHeight: 200.0,
flexibleSpace:
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) => DocumentPreview(
document: state.document,
fit: BoxFit.cover,
),
),
bottom: ColoredTabBar(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
tabBar: TabBar(
isScrollable: true,
tabs: [
Tab(
child: Text(
S.of(context).documentDetailsPageTabOverviewLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
Tab(
child: Text(
S.of(context).documentDetailsPageTabContentLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
Tab(
child: Text(
S.of(context).documentDetailsPageTabMetaDataLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
Tab(
child: Text(
S
.of(context)
.documentDetailsPageTabSimilarDocumentsLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
],
),
), ),
), ),
bottom: ColoredTabBar( ],
backgroundColor: body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
Theme.of(context).colorScheme.primaryContainer, builder: (context, state) {
tabBar: TabBar( return BlocProvider(
isScrollable: true, create: (context) => SimilarDocumentsCubit(
tabs: [ context.read(),
Tab( context.read(),
child: Text( documentId: state.document.id,
S.of(context).documentDetailsPageTabOverviewLabel, ),
style: TextStyle( child: TabBarView(
color: Theme.of(context) children: [
.colorScheme DocumentOverviewWidget(
.onPrimaryContainer, document: state.document,
), itemSpacing: _itemPadding,
queryString: widget.titleAndContentQueryString,
), ),
), DocumentContentWidget(
Tab( isFullContentLoaded: state.isFullContentLoaded,
child: Text( document: state.document,
S.of(context).documentDetailsPageTabContentLabel, fullContent: state.fullContent,
style: TextStyle( queryString: widget.titleAndContentQueryString,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
), ),
), DocumentMetaDataWidget(
Tab( document: state.document,
child: Text( itemSpacing: _itemPadding,
S.of(context).documentDetailsPageTabMetaDataLabel, metaData: _metaData,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
), ),
), const SimilarDocumentsView(),
Tab( ],
child: Text( ),
S );
.of(context) },
.documentDetailsPageTabSimilarDocumentsLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
],
),
),
), ),
],
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
return BlocProvider(
create: (context) => SimilarDocumentsCubit(
context.read(),
context.read(),
documentId: state.document.id,
),
child: TabBarView(
children: [
DocumentOverviewWidget(
document: state.document,
itemSpacing: _itemPadding,
queryString: widget.titleAndContentQueryString,
),
DocumentContentWidget(
isFullContentLoaded: state.isFullContentLoaded,
document: state.document,
fullContent: state.fullContent,
queryString: widget.titleAndContentQueryString,
),
DocumentMetaDataWidget(
document: state.document,
itemSpacing: _itemPadding,
metaData: _metaData,
),
const SimilarDocumentsView(),
],
),
);
},
), ),
), ),
), ),
@@ -25,18 +25,17 @@ class DocumentMetaDataWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, state) { builder: (context, connectivity) {
if (!state.isConnected) {
return const Center(
child: OfflineWidget(),
);
}
return FutureBuilder<DocumentMetaData>( return FutureBuilder<DocumentMetaData>(
future: metaData, future: metaData,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!connectivity.isConnected && !snapshot.hasData) {
return OfflineWidget();
}
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final meta = snapshot.data!; final meta = snapshot.data!;
return ListView( return ListView(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -55,7 +54,9 @@ class DocumentMetaDataWidget extends StatelessWidget {
label: Text(S label: Text(S
.of(context) .of(context)
.documentDetailsPageAssignAsnButtonLabel), .documentDetailsPageAssignAsnButtonLabel),
onPressed: () => _assignAsn(context), onPressed: connectivity.isConnected
? () => _assignAsn(context)
: null,
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
DetailsItem.text(DateFormat().format(document.modified), DetailsItem.text(DateFormat().format(document.modified),
@@ -36,11 +36,14 @@ class _DocumentViewState extends State<DocumentView> {
), ),
body: PdfView( body: PdfView(
builders: PdfViewBuilders<DefaultBuilderOptions>( builders: PdfViewBuilders<DefaultBuilderOptions>(
options: const DefaultBuilderOptions(), options: const DefaultBuilderOptions(
loaderSwitchDuration: Duration(milliseconds: 500),
),
pageLoaderBuilder: (context) => const Center( pageLoaderBuilder: (context) => const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
), ),
scrollDirection: Axis.vertical,
controller: _pdfController, controller: _pdfController,
), ),
); );
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
@@ -42,10 +43,18 @@ class DocumentsPage extends StatefulWidget {
} }
class _DocumentsPageState extends State<DocumentsPage> class _DocumentsPageState extends State<DocumentsPage>
with SingleTickerProviderStateMixin { with
SingleTickerProviderStateMixin,
DocumentPagingViewMixin<DocumentsPage, DocumentsCubit> {
late final TabController _tabController; late final TabController _tabController;
@override
ScrollController get pagingScrollController =>
_nestedScrollViewKey.currentState?.innerController ?? ScrollController();
final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey();
int _currentTab = 0; int _currentTab = 0;
bool _showBackToTopButton = false;
@override @override
void initState() { void initState() {
@@ -53,21 +62,40 @@ class _DocumentsPageState extends State<DocumentsPage>
_tabController = TabController( _tabController = TabController(
length: 2, length: 2,
vsync: this, vsync: this,
initialIndex: 0,
); );
try { Future.wait([
context.read<DocumentsCubit>().reload(); context.read<DocumentsCubit>().reload(),
context.read<SavedViewCubit>().reload(); context.read<SavedViewCubit>().reload(),
} on PaperlessServerException catch (error, stackTrace) { ]).onError<PaperlessServerException>(
showErrorMessage(context, error, stackTrace); (error, stackTrace) {
} showErrorMessage(context, error, stackTrace);
_tabController.addListener(_listenForTabChanges); return [];
},
);
_tabController.addListener(_tabChangesListener);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_nestedScrollViewKey.currentState!.innerController
..addListener(_scrollExtentListener)
..addListener(shouldLoadMoreDocumentsListener);
});
} }
void _listenForTabChanges() { void _tabChangesListener() {
setState(() { setState(() => _currentTab = _tabController.index);
_currentTab = _tabController.index; }
});
void _scrollExtentListener() {
if (pagingScrollController.position.pixels >
MediaQuery.of(context).size.height) {
if (!_showBackToTopButton) {
setState(() => _showBackToTopButton = true);
}
} else {
if (_showBackToTopButton) {
setState(() => _showBackToTopButton = false);
}
}
} }
@override @override
@@ -145,186 +173,100 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
return false; return false;
}, },
child: NestedScrollView( child: Stack(
headerSliverBuilder: (context, innerBoxIsScrolled) => [ children: [
SliverOverlapAbsorber( NestedScrollView(
// This widget takes the overlapping behavior of the SliverAppBar, key: _nestedScrollViewKey,
// and redirects it to the SliverOverlapInjector below. If it is headerSliverBuilder: (context, innerBoxIsScrolled) => [
// missing, then it is possible for the nested "inner" scroll view SliverOverlapAbsorber(
// below to end up under the SliverAppBar even when the inner // This widget takes the overlapping behavior of the SliverAppBar,
// scroll view thinks it has not been scrolled. // and redirects it to the SliverOverlapInjector below. If it is
// This is not necessary if the "headerSliverBuilder" only builds // missing, then it is possible for the nested "inner" scroll view
// widgets that do not overlap the next sliver. // below to end up under the SliverAppBar even when the inner
handle: NestedScrollView.sliverOverlapAbsorberHandleFor( // scroll view thinks it has not been scrolled.
context, // This is not necessary if the "headerSliverBuilder" only builds
), // widgets that do not overlap the next sliver.
sliver: BlocBuilder<DocumentsCubit, DocumentsState>( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
builder: (context, state) { context,
if (state.selection.isNotEmpty) { ),
return SliverAppBar( sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
floating: false, builder: (context, state) {
pinned: true, if (state.selection.isNotEmpty) {
leading: IconButton( return SliverAppBar(
icon: const Icon(Icons.close), floating: false,
onPressed: () => context pinned: true,
.read<DocumentsCubit>() leading: IconButton(
.resetSelection(), icon: const Icon(Icons.close),
), onPressed: () => context
title: Text( .read<DocumentsCubit>()
"${state.selection.length} ${S.of(context).documentsSelectedText}", .resetSelection(),
), ),
actions: [ title: Text(
IconButton( "${state.selection.length} ${S.of(context).documentsSelectedText}",
icon: const Icon(Icons.delete), ),
onPressed: () => _onDelete(state), actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _onDelete(state),
),
],
);
}
return SearchAppBar(
hintText:
S.of(context).documentSearchSearchDocuments,
onOpenSearch: showDocumentSearchPage,
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: S.of(context).documentsPageTitle),
Tab(text: S.of(context).savedViewsLabel),
],
), ),
], );
); },
),
),
],
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
} }
return SearchAppBar( final desiredTab =
hintText: S.of(context).documentSearchSearchDocuments, (metrics.pixels / metrics.maxScrollExtent).round();
onOpenSearch: showDocumentSearchPage, if (metrics.axis == Axis.horizontal &&
bottom: TabBar( _currentTab != desiredTab) {
controller: _tabController, setState(() => _currentTab = desiredTab);
tabs: [ }
Tab(text: S.of(context).documentsPageTitle), return false;
Tab(text: S.of(context).savedViewsLabel),
],
),
);
}, },
), child: TabBarView(
), controller: _tabController,
], children: [
body: NotificationListener<ScrollNotification>( Builder(
onNotification: (notification) { builder: (context) {
final metrics = notification.metrics; return _buildDocumentsTab(
if (metrics.maxScrollExtent == 0) { connectivityState,
return true;
}
final desiredTab =
(metrics.pixels / metrics.maxScrollExtent).round();
if (metrics.axis == Axis.horizontal &&
_currentTab != desiredTab) {
setState(() => _currentTab = desiredTab);
}
return false;
},
child: NotificationListener<ScrollMetricsNotification>(
onNotification: (notification) {
// Listen for scroll notifications to load new data.
// Scroll controller does not work here due to nestedscrollview limitations.
final currState = context.read<DocumentsCubit>().state;
final max = notification.metrics.maxScrollExtent;
if (max == 0 ||
_currentTab != 0 ||
currState.isLoading ||
currState.isLastPageLoaded) {
return true;
}
final offset = notification.metrics.pixels;
if (offset >= max * 0.7) {
context
.read<DocumentsCubit>()
.loadMore()
.onError<PaperlessServerException>(
(error, stackTrace) => showErrorMessage(
context, context,
error, );
stackTrace, },
), ),
); Builder(
} builder: (context) {
return false; return _buildSavedViewsTab(
}, connectivityState,
child: TabBarView( context,
controller: _tabController, );
children: [ },
Builder( ),
builder: (context) { ],
return RefreshIndicator( ),
edgeOffset: kToolbarHeight + kTextTabBarHeight,
onRefresh: _onReloadDocuments,
notificationPredicate: (_) =>
connectivityState.isConnected,
child: CustomScrollView(
key: const PageStorageKey<String>("documents"),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(
context),
),
_buildViewActions(),
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.hasLoaded &&
state.documents.isEmpty) {
return SliverToBoxAdapter(
child: DocumentsEmptyState(
state: state,
onReset: () {
context
.read<DocumentsCubit>()
.resetFilter();
},
),
);
}
return SliverAdaptiveDocumentsView(
viewType: state.viewType,
onTap: _openDetails,
onSelected: context
.read<DocumentsCubit>()
.toggleDocumentSelection,
hasInternetConnection:
connectivityState.isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected:
_addCorrespondentToFilter,
onDocumentTypeSelected:
_addDocumentTypeToFilter,
onStoragePathSelected:
_addStoragePathToFilter,
documents: state.documents,
hasLoaded: state.hasLoaded,
isLabelClickable: true,
isLoading: state.isLoading,
selectedDocumentIds: state.selectedIds,
);
},
),
],
),
);
},
),
Builder(
builder: (context) {
return RefreshIndicator(
edgeOffset: kToolbarHeight + kTextTabBarHeight,
onRefresh: _onReloadSavedViews,
notificationPredicate: (_) =>
connectivityState.isConnected,
child: CustomScrollView(
key: const PageStorageKey<String>("savedViews"),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(
context),
),
const SavedViewList(),
],
),
);
},
),
],
), ),
), ),
), if (_showBackToTopButton) _buildBackToTopAction(context),
],
), ),
), ),
); );
@@ -333,6 +275,109 @@ class _DocumentsPageState extends State<DocumentsPage>
); );
} }
Widget _buildBackToTopAction(BuildContext context) {
return Transform.translate(
offset: const Offset(24, -24),
child: Align(
alignment: Alignment.bottomLeft,
child: ActionChip(
backgroundColor: Theme.of(context).colorScheme.primary,
side: BorderSide.none,
avatar: Icon(
Icons.expand_less,
color: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () async {
await pagingScrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInExpo,
);
_nestedScrollViewKey.currentState?.outerController.jumpTo(0);
},
label: Text(
"Go to top", //TODO: INTL
style: DefaultTextStyle.of(context).style.apply(
color: Theme.of(context).colorScheme.onPrimary,
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
),
);
}
Widget _buildSavedViewsTab(
ConnectivityState connectivityState,
BuildContext context,
) {
return RefreshIndicator(
edgeOffset: kToolbarHeight + kTextTabBarHeight,
onRefresh: _onReloadSavedViews,
notificationPredicate: (_) => connectivityState.isConnected,
child: CustomScrollView(
key: const PageStorageKey<String>("savedViews"),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
const SavedViewList(),
],
),
);
}
Widget _buildDocumentsTab(
ConnectivityState connectivityState,
BuildContext context,
) {
return RefreshIndicator(
edgeOffset: kToolbarHeight + kTextTabBarHeight,
onRefresh: _onReloadDocuments,
notificationPredicate: (_) => connectivityState.isConnected,
child: CustomScrollView(
key: const PageStorageKey<String>("documents"),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
_buildViewActions(),
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.hasLoaded && state.documents.isEmpty) {
return SliverToBoxAdapter(
child: DocumentsEmptyState(
state: state,
onReset: context.read<DocumentsCubit>().resetFilter,
),
);
}
return SliverAdaptiveDocumentsView(
viewType: state.viewType,
onTap: _openDetails,
onSelected:
context.read<DocumentsCubit>().toggleDocumentSelection,
hasInternetConnection: connectivityState.isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected: _addCorrespondentToFilter,
onDocumentTypeSelected: _addDocumentTypeToFilter,
onStoragePathSelected: _addStoragePathToFilter,
documents: state.documents,
hasLoaded: state.hasLoaded,
isLabelClickable: true,
isLoading: state.isLoading,
selectedDocumentIds: state.selectedIds,
);
},
),
],
),
);
}
Widget _buildViewActions() { Widget _buildViewActions() {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Row( child: Row(
@@ -1,10 +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_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 {
@@ -41,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 {
@@ -69,6 +89,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
return _buildGridView(); return _buildGridView();
case ViewType.list: case ViewType.list:
return _buildListView(); return _buildListView();
case ViewType.detailed:
return _buildFullView(context);
} }
} }
@@ -101,9 +123,39 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
); );
} }
Widget _buildFullView(BuildContext context) {
if (showLoadingPlaceholder) {
//TODO: Build detailed loading animation
return DocumentsListLoadingWidget.sliver();
}
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: documents.length,
(context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentDetailedItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
),
);
},
),
);
}
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(
@@ -161,6 +213,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
return _buildGridView(); return _buildGridView();
case ViewType.list: case ViewType.list:
return _buildListView(); return _buildListView();
case ViewType.detailed:
return _buildFullView();
} }
} }
@@ -170,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,
@@ -194,11 +249,44 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
); );
} }
Widget _buildFullView() {
if (showLoadingPlaceholder) {
return DocumentsListLoadingWidget();
}
return ListView.builder(
padding: EdgeInsets.zero,
physics: const PageScrollPhysics(),
controller: scrollController,
primary: false,
itemCount: documents.length,
itemBuilder: (context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentDetailedItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
),
);
},
);
}
Widget _buildGridView() { Widget _buildGridView() {
if (showLoadingPlaceholder) { if (showLoadingPlaceholder) {
return DocumentGridLoadingWidget(); return DocumentGridLoadingWidget();
} }
return GridView.builder( return GridView.builder(
padding: EdgeInsets.zero,
controller: scrollController, controller: scrollController,
primary: false, primary: false,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@@ -2,51 +2,59 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
class DocumentPreview extends StatelessWidget { class DocumentPreview extends StatelessWidget {
final int id; final DocumentModel document;
final BoxFit fit; final BoxFit fit;
final Alignment alignment; final Alignment alignment;
final double borderRadius; final double borderRadius;
final bool enableHero; final bool enableHero;
final double scale;
const DocumentPreview({ const DocumentPreview({
super.key, super.key,
required this.id, required this.document,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.alignment = Alignment.center, this.alignment = Alignment.topCenter,
this.borderRadius = 8.0, this.borderRadius = 12.0,
this.enableHero = true, this.enableHero = true,
this.scale = 1.1,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!enableHero) { return HeroMode(
return _buildPreview(context); enabled: enableHero,
} child: Hero(
return Hero( tag: "thumb_${document.id}",
tag: "thumb_$id", child: _buildPreview(context),
child: _buildPreview(context), ),
); );
} }
ClipRRect _buildPreview(BuildContext context) { Widget _buildPreview(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
child: CachedNetworkImage( child: Transform.scale(
fit: fit, scale: scale,
alignment: Alignment.topCenter, child: CachedNetworkImage(
cacheKey: "thumb_$id", fit: fit,
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(id), alignment: alignment,
errorWidget: (ctxt, msg, __) => Text(msg), cacheKey: "thumb_${document.id}",
placeholder: (context, value) => Shimmer.fromColors( imageUrl: context
baseColor: Colors.grey[300]!, .read<PaperlessDocumentsApi>()
highlightColor: Colors.grey[100]!, .getThumbnailUrl(document.id),
child: const SizedBox(height: 100, width: 100), errorWidget: (ctxt, msg, __) => Text(msg),
placeholder: (context, value) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: const SizedBox(height: 100, width: 100),
),
cacheManager: context.watch<CacheManager>(),
), ),
cacheManager: context.watch<CacheManager>(),
), ),
); );
} }
@@ -0,0 +1,144 @@
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({
super.key,
required super.document,
required super.isSelected,
required super.isSelectionActive,
required super.isLabelClickable,
required super.enableHeroAnimation,
super.onCorrespondentSelected,
super.onDocumentTypeSelected,
super.onSelected,
super.onStoragePathSelected,
super.onTagSelected,
super.onTap,
});
@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: InkWell(
enableFeedback: true,
borderRadius: BorderRadius.circular(12),
onTap: () {
if (isSelectionActive) {
onSelected?.call(document);
} else {
onTap?.call(document);
}
},
onLongPress: () {
onSelected?.call(document);
},
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: double.infinity,
height: maxHeight / 2,
),
child: DocumentPreview(
document: document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
DateFormat.yMMMMd().format(document.created),
style: Theme.of(context)
.textTheme
.bodySmall
?.apply(color: Theme.of(context).hintColor),
),
if (document.archiveSerialNumber != null)
Row(
children: [
Text(
'#${document.archiveSerialNumber}',
style: Theme.of(context)
.textTheme
.bodySmall
?.apply(color: Theme.of(context).hintColor),
),
],
),
],
).paddedLTRB(8, 8, 8, 4),
Text(
document.title,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).paddedLTRB(8, 0, 8, 4),
Row(
children: [
const Icon(
Icons.person_outline,
size: 16,
).paddedOnly(right: 4.0),
CorrespondentWidget(
onSelected: onCorrespondentSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondentId: document.correspondent,
),
],
).paddedLTRB(8, 0, 8, 4),
Row(
children: [
const Icon(
Icons.description_outlined,
size: 16,
).paddedOnly(right: 4.0),
DocumentTypeWidget(
onSelected: onDocumentTypeSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
documentTypeId: document.documentType,
),
],
).paddedLTRB(8, 0, 8, 4),
TagsWidget(
isMultiLine: false,
tagIds: document.tags,
).padded(),
],
),
],
),
),
).padded();
}
}
@@ -25,66 +25,62 @@ class DocumentGridItem extends DocumentItem {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Padding(
onTap: _onTap, padding: const EdgeInsets.all(8.0),
onLongPress: onSelected != null ? () => onSelected!(document) : null, child: Card(
child: AbsorbPointer( elevation: 1.0,
absorbing: isSelectionActive, color: isSelected
child: Padding( ? Theme.of(context).colorScheme.inversePrimary
padding: const EdgeInsets.all(8.0), : Theme.of(context).cardColor,
child: Card( child: InkWell(
elevation: 1.0, borderRadius: BorderRadius.circular(12),
color: isSelected onTap: _onTap,
? Theme.of(context).colorScheme.inversePrimary onLongPress: onSelected != null ? () => onSelected!(document) : null,
: Theme.of(context).cardColor, child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ AspectRatio(
AspectRatio( aspectRatio: 1,
aspectRatio: 1, child: DocumentPreview(
child: DocumentPreview( document: document,
id: document.id, borderRadius: 12.0,
borderRadius: 12.0, enableHero: enableHeroAnimation,
enableHero: enableHeroAnimation, ),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CorrespondentWidget(
correspondentId: document.correspondent,
),
DocumentTypeWidget(
documentTypeId: document.documentType,
),
Text(
document.title,
maxLines: document.tags.isEmpty ? 3 : 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
TagsWidget(
tagIds: document.tags,
isMultiLine: false,
onTagSelected: onTagSelected,
),
const Spacer(),
Text(
DateFormat.yMMMd().format(document.created),
style: Theme.of(context).textTheme.bodySmall,
),
],
), ),
), ),
Expanded( ),
child: Padding( ],
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CorrespondentWidget(
correspondentId: document.correspondent,
),
DocumentTypeWidget(
documentTypeId: document.documentType,
),
Text(
document.title,
maxLines: document.tags.isEmpty ? 3 : 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
TagsWidget(
tagIds: document.tags,
isMultiLine: false,
onTagSelected: onTagSelected,
),
const Spacer(),
Text(
DateFormat.yMMMd().format(
document.created,
),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
],
),
), ),
), ),
), ),
@@ -122,7 +122,7 @@ class DocumentListItem extends DocumentItem {
aspectRatio: _a4AspectRatio, aspectRatio: _a4AspectRatio,
child: GestureDetector( child: GestureDetector(
child: DocumentPreview( child: DocumentPreview(
id: document.id, document: document,
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
enableHero: enableHeroAnimation, enableHero: enableHeroAnimation,
@@ -1,24 +1,15 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
import 'package:shimmer/shimmer.dart';
class DocumentGridLoadingWidget extends StatelessWidget class DocumentGridLoadingWidget extends StatelessWidget {
with DocumentItemPlaceholder {
final bool _isSliver; final bool _isSliver;
@override @override
final Random random = Random(1257195195); const DocumentGridLoadingWidget({super.key}) : _isSliver = false;
DocumentGridLoadingWidget({super.key}) : _isSliver = false;
DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true; const DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -41,8 +32,6 @@ class DocumentGridLoadingWidget extends StatelessWidget
} }
Widget _buildPlaceholderGridItem(BuildContext context) { Widget _buildPlaceholderGridItem(BuildContext context) {
final values = nextValues;
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Card( child: Card(
@@ -68,21 +57,25 @@ class DocumentGridLoadingWidget extends StatelessWidget
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TextPlaceholder( const TextPlaceholder(
length: values.correspondentLength, length: 70,
fontSize: 16,
).padded(1),
const TextPlaceholder(
length: 50,
fontSize: 16, fontSize: 16,
).padded(1), ).padded(1),
TextPlaceholder( TextPlaceholder(
length: values.titleLength, length: 200,
fontSize: 16, fontSize:
Theme.of(context).textTheme.titleMedium?.fontSize ??
10,
).padded(1),
const Spacer(),
const TagsPlaceholder(
count: 2,
dense: true,
), ),
if (values.tagCount > 0) ...[
const Spacer(),
TagsPlaceholder(
count: values.tagCount,
dense: true,
),
],
const Spacer(), const Spacer(),
TextPlaceholder( TextPlaceholder(
length: 100, length: 100,
@@ -1,30 +0,0 @@
import 'dart:math';
mixin DocumentItemPlaceholder {
static const _tags = [" ", " ", " "];
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
static const _correspondentLengths = <double>[120.0, 80.0, 40.0];
Random get random;
RandomDocumentItemPlaceholderValues get nextValues {
return RandomDocumentItemPlaceholderValues(
tagCount: random.nextInt(_tags.length + 1),
correspondentLength: _correspondentLengths[
random.nextInt(_correspondentLengths.length - 1)],
titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)],
);
}
}
class RandomDocumentItemPlaceholderValues {
final int tagCount;
final double correspondentLength;
final double titleLength;
RandomDocumentItemPlaceholderValues({
required this.tagCount,
required this.correspondentLength,
required this.titleLength,
});
}
@@ -1,21 +1,13 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
class DocumentsListLoadingWidget extends StatelessWidget class DocumentsListLoadingWidget extends StatelessWidget {
with DocumentItemPlaceholder {
final bool _isSliver; final bool _isSliver;
DocumentsListLoadingWidget({super.key}) : _isSliver = false; const DocumentsListLoadingWidget({super.key}) : _isSliver = false;
DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true; const DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
@override
final Random random = Random(1209571050);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -35,26 +27,31 @@ class DocumentsListLoadingWidget extends StatelessWidget
Widget _buildFakeListItem(BuildContext context) { Widget _buildFakeListItem(BuildContext context) {
const fontSize = 14.0; const fontSize = 14.0;
final values = nextValues;
return ShimmerPlaceholder( return ShimmerPlaceholder(
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.all(8), contentPadding: const EdgeInsets.all(8),
dense: true, dense: true,
isThreeLine: true, isThreeLine: true,
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
child: Container( child: Container(
color: Colors.white, color: Colors.white,
height: double.infinity, height: double.infinity,
width: 35, width: 35,
), ),
), ),
title: Row( title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TextPlaceholder( const TextPlaceholder(
length: values.correspondentLength, length: 120,
fontSize: fontSize, fontSize: fontSize,
), ),
const SizedBox(height: 2),
TextPlaceholder(
length: 220,
fontSize: Theme.of(context).textTheme.titleMedium!.fontSize!,
),
], ],
), ),
subtitle: Padding( subtitle: Padding(
@@ -63,14 +60,10 @@ class DocumentsListLoadingWidget extends StatelessWidget
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
TagsPlaceholder(count: 2, dense: true),
SizedBox(height: 2),
TextPlaceholder( TextPlaceholder(
length: values.titleLength, length: 250,
fontSize: fontSize,
),
if (values.tagCount > 0)
TagsPlaceholder(count: values.tagCount, dense: true),
TextPlaceholder(
length: 100,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!, fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,
), ),
], ],
@@ -15,6 +15,7 @@ class TagsPlaceholder extends StatelessWidget {
return SizedBox( return SizedBox(
height: 32, height: 32,
child: ListView.separated( child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: count, itemCount: count,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => FilterChip( itemBuilder: (context, index) => FilterChip(
@@ -1,9 +1,4 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class TextPlaceholder extends StatelessWidget { class TextPlaceholder extends StatelessWidget {
final double length; final double length;
@@ -14,13 +14,65 @@ class ViewTypeSelectionWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final next = viewType.toggle(); late final IconData icon;
final icon = next == ViewType.grid ? Icons.grid_view_rounded : Icons.list; switch (viewType) {
return IconButton( case ViewType.grid:
icon = Icons.grid_view_rounded;
break;
case ViewType.list:
icon = Icons.list;
break;
case ViewType.detailed:
icon = Icons.article_outlined;
break;
}
return PopupMenuButton<ViewType>(
initialValue: viewType,
icon: Icon(icon), icon: Icon(icon),
onPressed: () { itemBuilder: (context) => [
_buildViewTypeOption(
context,
type: ViewType.list,
label: 'List', //TODO: INTL
icon: Icons.list,
),
_buildViewTypeOption(
context,
type: ViewType.grid,
label: 'Grid', //TODO: INTL
icon: Icons.grid_view_rounded,
),
_buildViewTypeOption(
context,
type: ViewType.detailed,
label: 'Detailed', //TODO: INTL
icon: Icons.article_outlined,
),
],
onSelected: (next) {
onChanged(next); onChanged(next);
}, },
); );
} }
PopupMenuItem<ViewType> _buildViewTypeOption(
BuildContext context, {
required ViewType type,
required String label,
required IconData icon,
}) {
final selected = type == viewType;
return PopupMenuItem(
value: type,
child: ListTile(
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,
),
);
}
} }
+19 -21
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: [
@@ -5,10 +5,10 @@ import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/dart_extensions.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart';
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
@@ -24,7 +24,8 @@ class InboxPage extends StatefulWidget {
State<InboxPage> createState() => _InboxPageState(); State<InboxPage> createState() => _InboxPageState();
} }
class _InboxPageState extends State<InboxPage> with DocumentPagingViewMixin { class _InboxPageState extends State<InboxPage>
with DocumentPagingViewMixin<InboxPage, InboxCubit> {
@override @override
final pagingScrollController = ScrollController(); final pagingScrollController = ScrollController();
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>(); final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@@ -54,7 +54,7 @@ class _InboxItemState extends State<InboxItem> {
AspectRatio( AspectRatio(
aspectRatio: InboxItem._a4AspectRatio, aspectRatio: InboxItem._a4AspectRatio,
child: DocumentPreview( child: DocumentPreview(
id: widget.document.id, document: widget.document,
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
enableHero: false, enableHero: false,
@@ -31,6 +31,8 @@ class DocumentTypeWidget extends StatelessWidget {
state.labels[documentTypeId]?.toString() ?? "-", state.labels[documentTypeId]?.toString() ?? "-",
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium) style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
?.copyWith(color: Theme.of(context).colorScheme.tertiary), ?.copyWith(color: Theme.of(context).colorScheme.tertiary),
overflow: TextOverflow.ellipsis,
maxLines: 1,
); );
}, },
), ),
@@ -16,7 +16,7 @@ class LinkedDocumentsPage extends StatefulWidget {
} }
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
with DocumentPagingViewMixin { with DocumentPagingViewMixin<LinkedDocumentsPage, LinkedDocumentsCubit> {
@override @override
final pagingScrollController = ScrollController(); final pagingScrollController = ScrollController();
@@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> { mixin DocumentPagingViewMixin<T extends StatefulWidget,
Bloc extends DocumentPagingBlocMixin> on State<T> {
ScrollController get pagingScrollController; ScrollController get pagingScrollController;
@override @override
@@ -20,7 +20,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> {
super.dispose(); super.dispose();
} }
DocumentPagingBlocMixin get _bloc => context.read<DocumentPagingBlocMixin>(); DocumentPagingBlocMixin get _bloc => context.read<Bloc>();
void shouldLoadMoreDocumentsListener() async { void shouldLoadMoreDocumentsListener() async {
if (shouldLoadMoreDocuments) { if (shouldLoadMoreDocuments) {
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
@@ -12,50 +13,55 @@ class SavedViewList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final savedViewCubit = context.read<SavedViewCubit>(); final savedViewCubit = context.read<SavedViewCubit>();
return BlocBuilder<SavedViewCubit, SavedViewState>( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, state) { builder: (context, connectivity) {
if (state.value.isEmpty) { return BlocBuilder<SavedViewCubit, SavedViewState>(
return SliverToBoxAdapter( builder: (context, state) {
child: HintCard( if (state.value.isEmpty) {
hintText: S.of(context).savedViewsEmptyStateText, return SliverToBoxAdapter(
), child: HintCard(
); hintText: S.of(context).savedViewsEmptyStateText,
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final view = state.value.values.elementAt(index);
return ListTile(
title: Text(view.name),
subtitle: Text(
S
.of(context)
.savedViewsFiltersSetCount(view.filterRules.length),
), ),
onTap: () { );
Navigator.of(context).push( }
MaterialPageRoute( return SliverList(
builder: (context) => MultiBlocProvider( delegate: SliverChildBuilderDelegate(
providers: [ (context, index) {
BlocProvider( final view = state.value.values.elementAt(index);
create: (context) => SavedViewDetailsCubit( return ListTile(
context.read(), enabled: connectivity.isConnected,
context.read(), title: Text(view.name),
savedView: view, subtitle: Text(
S
.of(context)
.savedViewsFiltersSetCount(view.filterRules.length),
),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SavedViewDetailsCubit(
context.read(),
context.read(),
savedView: view,
),
),
],
child: SavedViewDetailsPage(
onDelete: savedViewCubit.remove,
), ),
), ),
],
child: SavedViewDetailsPage(
onDelete: savedViewCubit.remove,
), ),
), );
), },
); );
}, },
); childCount: state.value.length,
}, ),
childCount: state.value.length, );
), },
); );
}, },
); );
@@ -22,7 +22,7 @@ class SavedViewDetailsPage extends StatefulWidget {
} }
class _SavedViewDetailsPageState extends State<SavedViewDetailsPage> class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
with DocumentPagingViewMixin { with DocumentPagingViewMixin<SavedViewDetailsPage, SavedViewDetailsCubit> {
@override @override
final pagingScrollController = ScrollController(); final pagingScrollController = ScrollController();
@@ -56,7 +56,7 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
onChanged: cubit.setViewType, onChanged: cubit.setViewType,
); );
}, },
) ),
], ],
), ),
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>( body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
@@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
@@ -29,13 +32,13 @@ class _SearchAppBarState extends State<SearchAppBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverAppBar( return SliverAppBar(
automaticallyImplyLeading: false,
floating: true, floating: true,
pinned: true, pinned: true,
snap: true, snap: true,
automaticallyImplyLeading: false,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
title: SearchBar( title: SearchBar(
height: kToolbarHeight - 8, height: kToolbarHeight - 12,
supportingText: widget.hintText, supportingText: widget.hintText,
onTap: () => widget.onOpenSearch(context), onTap: () => widget.onOpenSearch(context),
leadingIcon: IconButton( leadingIcon: IconButton(
@@ -43,17 +46,22 @@ class _SearchAppBarState extends State<SearchAppBar> {
onPressed: Scaffold.of(context).openDrawer, onPressed: Scaffold.of(context).openDrawer,
), ),
trailingIcon: IconButton( trailingIcon: IconButton(
icon: const CircleAvatar( icon: BlocBuilder<PaperlessServerInformationCubit,
child: Text("A"), PaperlessServerInformationState>(
builder: (context, state) {
return CircleAvatar(
child: Text(state.information?.userInitials ?? ''),
);
},
), ),
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AccountSettingsDialog(), builder: (context) => const AccountSettingsDialog(),
); );
}, },
), ),
).paddedOnly(top: 4, bottom: 4), ).paddedOnly(top: 8, bottom: 4),
bottom: widget.bottom, bottom: widget.bottom,
); );
} }
+3 -2
View File
@@ -1,8 +1,9 @@
enum ViewType { enum ViewType {
grid, grid,
list; list,
detailed;
ViewType toggle() { ViewType toggle() {
return this == grid ? list : grid; return ViewType.values[(index + 1) % ViewType.values.length];
} }
} }
@@ -35,10 +35,7 @@ class AccountSettingsDialog extends StatelessWidget {
children: [ children: [
ExpansionTile( ExpansionTile(
leading: CircleAvatar( leading: CircleAvatar(
child: Text(state.information?.username child: Text(state.information?.userInitials ?? ''),
?.toUpperCase()
.substring(0, 1) ??
''),
), ),
title: Text(state.information?.username ?? ''), title: Text(state.information?.username ?? ''),
subtitle: Text(state.information?.host ?? ''), subtitle: Text(state.information?.host ?? ''),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
@@ -17,7 +18,7 @@ class SimilarDocumentsView extends StatefulWidget {
} }
class _SimilarDocumentsViewState extends State<SimilarDocumentsView> class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
with DocumentPagingViewMixin { with DocumentPagingViewMixin<SimilarDocumentsView, SimilarDocumentsCubit> {
@override @override
final pagingScrollController = ScrollController(); final pagingScrollController = ScrollController();
@@ -33,44 +34,50 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>( return BlocConsumer<ConnectivityCubit, ConnectivityState>(
builder: (context, state) { listenWhen: (previous, current) =>
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { !previous.isConnected && current.isConnected,
return DocumentsEmptyState( listener: (context, state) =>
state: state, context.read<SimilarDocumentsCubit>().initialize(),
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter( builder: (context, connectivity) {
filter: DocumentFilter.initial.copyWith( return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
moreLike: () => builder: (context, state) {
context.read<SimilarDocumentsCubit>().documentId, if (!connectivity.isConnected && !state.hasLoaded) {
), return const OfflineWidget();
), }
); if (state.hasLoaded &&
} !state.isLoading &&
state.documents.isEmpty) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return DocumentsEmptyState(
builder: (context, connectivity) { state: state,
return CustomScrollView( onReset: () => context
controller: pagingScrollController, .read<SimilarDocumentsCubit>()
slivers: [ .updateFilter(
SliverAdaptiveDocumentsView( filter: DocumentFilter.initial.copyWith(
documents: state.documents, moreLike: () =>
hasInternetConnection: connectivity.isConnected, context.read<SimilarDocumentsCubit>().documentId,
isLabelClickable: false,
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
enableHeroAnimation: false,
onTap: (document) {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
), ),
); ),
}, );
), }
], return DefaultAdaptiveDocumentsView(
scrollController: pagingScrollController,
documents: state.documents,
hasInternetConnection: connectivity.isConnected,
isLabelClickable: false,
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
enableHeroAnimation: false,
onTap: (document) {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
),
);
},
); );
}, },
); );
@@ -9,6 +9,10 @@ class PaperlessServerInformationModel {
final String? username; final String? username;
final String? host; final String? host;
String? get userInitials {
return username?.substring(0, 1).toUpperCase();
}
PaperlessServerInformationModel({ PaperlessServerInformationModel({
this.host, this.host,
this.username, this.username,
@@ -24,7 +24,7 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
} on DioError catch (error) { } on DioError catch (error) {
if (error.error is PaperlessServerException || if (error.error is PaperlessServerException ||
error.error is Map<String, String>) { error.error is Map<String, String>) {
throw error.error; throw error.error as Map<String, String>;
} else { } else {
throw PaperlessServerException( throw PaperlessServerException(
ErrorCode.authenticationFailed, ErrorCode.authenticationFailed,
@@ -2,14 +2,10 @@ import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/constants.dart'; import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
static const _dateTimeConverter = LocalDateTimeJsonConverter();
final Dio client; final Dio client;
PaperlessDocumentsApiImpl(this.client); PaperlessDocumentsApiImpl(this.client);
@@ -65,7 +61,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
); );
} }
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -82,7 +78,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
throw const PaperlessServerException(ErrorCode.documentUpdateFailed); throw const PaperlessServerException(ErrorCode.documentUpdateFailed);
} }
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -109,7 +105,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
throw const PaperlessServerException(ErrorCode.documentLoadFailed); throw const PaperlessServerException(ErrorCode.documentLoadFailed);
} }
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -123,7 +119,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
throw const PaperlessServerException(ErrorCode.documentDeleteFailed); throw const PaperlessServerException(ErrorCode.documentDeleteFailed);
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -150,7 +146,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
throw const PaperlessServerException(ErrorCode.documentPreviewFailed); throw const PaperlessServerException(ErrorCode.documentPreviewFailed);
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -172,7 +168,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} on PaperlessServerException { } on PaperlessServerException {
throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed); throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed);
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -191,7 +187,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
); );
} }
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -208,7 +204,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
); );
return response.data; return response.data;
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -222,7 +218,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
response.data as Map<String, dynamic>, response.data as Map<String, dynamic>,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -241,7 +237,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
throw const PaperlessServerException(ErrorCode.autocompleteQueryError); throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -256,7 +252,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
throw const PaperlessServerException(ErrorCode.suggestionsQueryError); throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -270,7 +266,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
return null; return null;
} }
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
} }
@@ -103,7 +103,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -122,7 +122,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -142,7 +142,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -160,7 +160,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -178,7 +178,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -195,7 +195,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -215,7 +215,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -235,7 +235,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -256,7 +256,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -273,7 +273,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -316,7 +316,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -333,7 +333,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
} }
throw const PaperlessServerException(ErrorCode.unknown); throw const PaperlessServerException(ErrorCode.unknown);
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
} }
@@ -39,7 +39,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -55,7 +55,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -29,7 +29,7 @@ Future<T> getSingleResult<T>(
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
@@ -66,12 +66,13 @@ Future<List<T>> getCollection<T>(
httpStatusCode: response.statusCode, httpStatusCode: response.statusCode,
); );
} on DioError catch (err) { } on DioError catch (err) {
throw err.error; throw err.error!;
} }
} }
List<T> _collectionFromJson<T>( List<T> _collectionFromJson<T>(
_CollectionFromJsonSerializationParams<T> params) { _CollectionFromJsonSerializationParams<T> params,
) {
return params.list.map<T>((result) => params.fromJson(result)).toList(); return params.list.map<T>((result) => params.fromJson(result)).toList();
} }
+1 -1
View File
@@ -17,7 +17,7 @@ dependencies:
http: ^0.13.5 http: ^0.13.5
json_annotation: ^4.7.0 json_annotation: ^4.7.0
intl: ^0.17.0 intl: ^0.17.0
dio: ^4.0.6 dio: ^5.0.0
collection: ^1.17.0 collection: ^1.17.0
jiffy: ^5.0.0 jiffy: ^5.0.0
+38 -70
View File
@@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
asn1lib: asn1lib:
dependency: transitive dependency: transitive
description: description:
@@ -101,18 +101,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: bloc name: bloc
sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.0" version: "8.1.1"
bloc_test: bloc_test:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: bloc_test name: bloc_test
sha256: "622b97678bf8c06a94f4c26a89ee9ebf7319bf775383dee2233e86e1f94ee28d" sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.1.0" version: "9.1.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -253,50 +253,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: connectivity_plus name: connectivity_plus
sha256: "3f8fe4e504c2d33696dac671a54909743bc6a902a9bb0902306f7a2aed7e528e" sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.9" version: "3.0.3"
connectivity_plus_linux:
dependency: transitive
description:
name: connectivity_plus_linux
sha256: "3caf859d001f10407b8e48134c761483e4495ae38094ffcca97193f6c271f5e2"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
connectivity_plus_macos:
dependency: transitive
description:
name: connectivity_plus_macos
sha256: "488d2de1e47e1224ad486e501b20b088686ba1f4ee9c4420ecbc3b9824f0b920"
url: "https://pub.dev"
source: hosted
version: "1.2.6"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: connectivity_plus_platform_interface name: connectivity_plus_platform_interface
sha256: b8795b9238bf83b64375f63492034cb3d8e222af4d9ce59dda085edf038fa06f sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.4"
connectivity_plus_web:
dependency: transitive
description:
name: connectivity_plus_web
sha256: "81332be1b4baf8898fed17bb4fdef27abb7c6fd990bf98c54fd978478adf2f1a"
url: "https://pub.dev"
source: hosted
version: "1.2.5"
connectivity_plus_windows:
dependency: transitive
description:
name: connectivity_plus_windows
sha256: "535b0404b4d5605c4dd8453d67e5d6d2ea0dd36e3b477f50f31af51b0aeab9dd"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@@ -309,10 +277,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: coverage name: coverage
sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040" sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.2" version: "1.6.3"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@@ -341,18 +309,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: dart_code_metrics name: dart_code_metrics
sha256: bb4ec5e729788dde5f7e8e9df4c05ec3b78532a5763e635337153ce40085514b sha256: "026e28da197a03caeccccc0b174ec98ef03da3c81c4543314d7add121aab4375"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.1" version: "5.6.0"
dart_code_metrics_presets: dart_code_metrics_presets:
dependency: transitive dependency: transitive
description: description:
name: dart_code_metrics_presets name: dart_code_metrics_presets
sha256: "43dc1fdcb424fc3aa79964304d09eeda4f199351c52cdc854f8228a9d0296b60" sha256: "9c51724f836aebc4465228954cb5757e5a99737af26a452b5dec0a2d5d0b4d66"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@@ -437,10 +405,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.6" version: "5.0.0"
dots_indicator: dots_indicator:
dependency: transitive dependency: transitive
description: description:
@@ -547,10 +515,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_bloc name: flutter_bloc
sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557" sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.1" version: "8.1.2"
flutter_blurhash: flutter_blurhash:
dependency: transitive dependency: transitive
description: description:
@@ -735,18 +703,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fluttertoast name: fluttertoast
sha256: "7cc92eabe01e3f1babe1571c5560b135dfc762a34e41e9056881e2196b178ec1" sha256: "774fa28b07f3a82c93596bc137be33189fec578ed3447a93a5a11c93435de394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.2" version: "8.1.3"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: font_awesome_flutter name: font_awesome_flutter
sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc" sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.0" version: "10.4.0"
form_builder_validators: form_builder_validators:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -836,10 +804,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: hydrated_bloc name: hydrated_bloc
sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7" sha256: eb92d88061b6b911c48779b08a91c8a9f3a3aa8475f80d9380045375d9876536
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" version: "9.1.0"
image: image:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -937,10 +905,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_android name: local_auth_android
sha256: ba48fe0e1cae140a0813ce68c2540250d7f573a8ae4d4b6c681b2d2583584953 sha256: cfcbc4936e288d61ef85a04feef6b95f49ba496d4fd98364e6abafb462b06a1f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.17" version: "1.0.18"
local_auth_ios: local_auth_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1176,10 +1144,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.7" version: "2.1.8"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1312,10 +1280,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: pretty_dio_logger name: pretty_dio_logger
sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0-beta-1" version: "1.3.1"
process: process:
dependency: transitive dependency: transitive
description: description:
@@ -1392,10 +1360,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.0" version: "6.3.1"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1685,10 +1653,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.8" version: "6.1.9"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
@@ -1701,10 +1669,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.18" version: "6.1.0"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
+2 -2
View File
@@ -60,7 +60,7 @@ dependencies:
package_info_plus: ^1.4.3+1 package_info_plus: ^1.4.3+1
font_awesome_flutter: ^10.1.0 font_awesome_flutter: ^10.1.0
local_auth: ^2.1.2 local_auth: ^2.1.2
connectivity_plus: ^2.3.9 connectivity_plus: ^3.0.3
flutter_native_splash: ^2.2.11 flutter_native_splash: ^2.2.11
share_plus: ^6.2.0 share_plus: ^6.2.0
@@ -77,7 +77,7 @@ dependencies:
badges: ^2.0.3 badges: ^2.0.3
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
provider: ^6.0.5 provider: ^6.0.5
dio: ^4.0.6 dio: ^5.0.0
hydrated_bloc: ^9.0.0 hydrated_bloc: ^9.0.0
json_annotation: ^4.7.0 json_annotation: ^4.7.0
pretty_dio_logger: ^1.2.0-beta-1 pretty_dio_logger: ^1.2.0-beta-1
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
pushd ../
pushd packages/paperless_api pushd packages/paperless_api
flutter pub get flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs flutter pub run build_runner build --delete-conflicting-outputs