mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2026-01-31 10:25:03 -06:00
Merge pull request #99 from astubenbord/feature/detailed-view-type
Feature: Detailed view type
This commit is contained in:
@@ -75,22 +75,17 @@ To get a local copy up and running follow these simple steps.
|
||||
|
||||
### Prerequisites
|
||||
* 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
|
||||
1. First, clone the repository:
|
||||
```sh
|
||||
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
|
||||
```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 local packages.
|
||||
|
||||
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 don't want to use submodules, you can also run the following commands using your local flutter installation:
|
||||
If you want to manually install dependencies and build generated files, you can also run the following commands:
|
||||
|
||||
#### Inside the `packages/paperless_api/` folder:
|
||||
2. Install the dependencies for `paperless_api`
|
||||
|
||||
@@ -22,7 +22,7 @@ class DioHttpErrorInterceptor extends Interceptor {
|
||||
DioError(
|
||||
error: const PaperlessServerException(ErrorCode.deviceOffline),
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.connectTimeout,
|
||||
type: DioErrorType.connectionTimeout,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class DioHttpErrorInterceptor extends Interceptor {
|
||||
DioError(
|
||||
error: errorMessages,
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.response,
|
||||
type: DioErrorType.badResponse,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class DioHttpErrorInterceptor extends Interceptor {
|
||||
handler.reject(
|
||||
DioError(
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.response,
|
||||
type: DioErrorType.badResponse,
|
||||
error: const PaperlessServerException(
|
||||
ErrorCode.missingClientCertificate),
|
||||
),
|
||||
|
||||
@@ -28,10 +28,12 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
bool _shouldRetryOnHttpException(DioError err) {
|
||||
return err.type == DioErrorType.other &&
|
||||
((err.error is HttpException &&
|
||||
err.message.contains(
|
||||
'Connection closed before full header was received')));
|
||||
return err.type == DioErrorType.unknown &&
|
||||
(err.error is HttpException &&
|
||||
(err.message?.contains(
|
||||
'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(
|
||||
ReachabilityStatus.connectionTimeout,
|
||||
err,
|
||||
@@ -55,6 +55,6 @@ void _rejectWithStatus(
|
||||
error: reachabilityStatus,
|
||||
requestOptions: err.requestOptions,
|
||||
response: err.response,
|
||||
type: DioErrorType.other,
|
||||
type: DioErrorType.unknown,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/adapter.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.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/features/login/model/client_certificate.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
@@ -19,10 +19,12 @@ class SessionManager {
|
||||
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//en- and decoded by utf8 by default
|
||||
final Dio dio = Dio(BaseOptions());
|
||||
dio.options.receiveTimeout = const Duration(seconds: 25).inMilliseconds;
|
||||
final Dio dio = Dio(
|
||||
BaseOptions(contentType: Headers.jsonContentType),
|
||||
);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 25);
|
||||
dio.options.responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
|
||||
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||
dio.interceptors.addAll([
|
||||
...interceptors,
|
||||
@@ -59,7 +61,7 @@ class SessionManager {
|
||||
clientCertificate.bytes,
|
||||
password: clientCertificate.passphrase,
|
||||
);
|
||||
final adapter = DefaultHttpClientAdapter()
|
||||
final adapter = IOHttpClientAdapter()
|
||||
..onHttpClientCreate = (client) => HttpClient(context: context)
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
@@ -72,7 +74,9 @@ class SessionManager {
|
||||
}
|
||||
|
||||
if (authToken != null) {
|
||||
client.options.headers.addAll({'Authorization': 'Token $authToken'});
|
||||
client.options.headers.addAll({
|
||||
HttpHeaders.authorizationHeader: 'Token $authToken',
|
||||
});
|
||||
}
|
||||
|
||||
if (serverInformation != null) {
|
||||
@@ -81,9 +85,9 @@ class SessionManager {
|
||||
}
|
||||
|
||||
void resetSettings() {
|
||||
client.httpClientAdapter = DefaultHttpClientAdapter();
|
||||
client.httpClientAdapter = IOHttpClientAdapter();
|
||||
client.options.baseUrl = '';
|
||||
client.options.headers.remove('Authorization');
|
||||
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||
serverInformation = PaperlessServerInformationModel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/adapter.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:paperless_mobile/core/global/os_error_codes.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart';
|
||||
@@ -71,8 +69,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
SessionManager manager =
|
||||
SessionManager([ServerReachabilityErrorInterceptor()])
|
||||
..updateSettings(clientCertificate: clientCertificate)
|
||||
..client.options.connectTimeout = 5000
|
||||
..client.options.receiveTimeout = 5000;
|
||||
..client.options.connectTimeout = const Duration(seconds: 5)
|
||||
..client.options.receiveTimeout = const Duration(seconds: 5);
|
||||
|
||||
final response = await manager.client.get('$serverAddress/api/');
|
||||
if (response.statusCode == 200) {
|
||||
@@ -80,7 +78,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
}
|
||||
return ReachabilityStatus.notReachable;
|
||||
} on DioError catch (error) {
|
||||
if (error.type == DioErrorType.other &&
|
||||
if (error.type == DioErrorType.unknown &&
|
||||
error.error is ReachabilityStatus) {
|
||||
return error.error as ReachabilityStatus;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,22 @@ extension WidgetPadding on Widget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget paddedSymmetrically({double horizontal = 0.0, double vertical = 0.0}) {
|
||||
Widget paddedSymmetrically({
|
||||
double horizontal = 0.0,
|
||||
double vertical = 0.0,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
Widget paddedOnly(
|
||||
{double top = 0.0,
|
||||
double bottom = 0.0,
|
||||
double left = 0.0,
|
||||
double right = 0.0}) {
|
||||
Widget paddedOnly({
|
||||
double top = 0.0,
|
||||
double bottom = 0.0,
|
||||
double left = 0.0,
|
||||
double right = 0.0,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: top,
|
||||
@@ -30,6 +34,13 @@ extension WidgetPadding on Widget {
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
Widget paddedLTRB(double left, double top, double right, double bottom) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(left, top, right, bottom),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension WidgetsPadding on List<Widget> {
|
||||
|
||||
@@ -49,6 +49,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMetaData();
|
||||
}
|
||||
|
||||
void _loadMetaData() {
|
||||
_metaData = context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
|
||||
@@ -64,108 +68,117 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
},
|
||||
child: DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
||||
bottomNavigationBar: _buildBottomAppBar(),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverAppBar(
|
||||
leading: const BackButton(),
|
||||
floating: true,
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
flexibleSpace:
|
||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) => DocumentPreview(
|
||||
id: state.document.id,
|
||||
fit: BoxFit.cover,
|
||||
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) {
|
||||
_loadMetaData();
|
||||
setState(() {});
|
||||
},
|
||||
child: Scaffold(
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
||||
bottomNavigationBar: _buildBottomAppBar(),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverAppBar(
|
||||
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:
|
||||
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,
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context).documentDetailsPageTabContentLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
DocumentContentWidget(
|
||||
isFullContentLoaded: state.isFullContentLoaded,
|
||||
document: state.document,
|
||||
fullContent: state.fullContent,
|
||||
queryString: widget.titleAndContentQueryString,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context).documentDetailsPageTabMetaDataLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
DocumentMetaDataWidget(
|
||||
document: state.document,
|
||||
itemSpacing: _itemPadding,
|
||||
metaData: _metaData,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S
|
||||
.of(context)
|
||||
.documentDetailsPageTabSimilarDocumentsLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SimilarDocumentsView(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, state) {
|
||||
if (!state.isConnected) {
|
||||
return const Center(
|
||||
child: OfflineWidget(),
|
||||
);
|
||||
}
|
||||
builder: (context, connectivity) {
|
||||
return FutureBuilder<DocumentMetaData>(
|
||||
future: metaData,
|
||||
builder: (context, snapshot) {
|
||||
if (!connectivity.isConnected && !snapshot.hasData) {
|
||||
return OfflineWidget();
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final meta = snapshot.data!;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -55,7 +54,9 @@ class DocumentMetaDataWidget extends StatelessWidget {
|
||||
label: Text(S
|
||||
.of(context)
|
||||
.documentDetailsPageAssignAsnButtonLabel),
|
||||
onPressed: () => _assignAsn(context),
|
||||
onPressed: connectivity.isConnected
|
||||
? () => _assignAsn(context)
|
||||
: null,
|
||||
),
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(DateFormat().format(document.modified),
|
||||
|
||||
@@ -36,11 +36,14 @@ class _DocumentViewState extends State<DocumentView> {
|
||||
),
|
||||
body: PdfView(
|
||||
builders: PdfViewBuilders<DefaultBuilderOptions>(
|
||||
options: const DefaultBuilderOptions(),
|
||||
options: const DefaultBuilderOptions(
|
||||
loaderSwitchDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
pageLoaderBuilder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: _pdfController,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||
import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
||||
@@ -42,10 +43,18 @@ class DocumentsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DocumentsPageState extends State<DocumentsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with
|
||||
SingleTickerProviderStateMixin,
|
||||
DocumentPagingViewMixin<DocumentsPage, DocumentsCubit> {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
ScrollController get pagingScrollController =>
|
||||
_nestedScrollViewKey.currentState?.innerController ?? ScrollController();
|
||||
|
||||
final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey();
|
||||
int _currentTab = 0;
|
||||
bool _showBackToTopButton = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -53,21 +62,40 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
_tabController = TabController(
|
||||
length: 2,
|
||||
vsync: this,
|
||||
initialIndex: 0,
|
||||
);
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
context.read<SavedViewCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
_tabController.addListener(_listenForTabChanges);
|
||||
Future.wait([
|
||||
context.read<DocumentsCubit>().reload(),
|
||||
context.read<SavedViewCubit>().reload(),
|
||||
]).onError<PaperlessServerException>(
|
||||
(error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
_tabController.addListener(_tabChangesListener);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_nestedScrollViewKey.currentState!.innerController
|
||||
..addListener(_scrollExtentListener)
|
||||
..addListener(shouldLoadMoreDocumentsListener);
|
||||
});
|
||||
}
|
||||
|
||||
void _listenForTabChanges() {
|
||||
setState(() {
|
||||
_currentTab = _tabController.index;
|
||||
});
|
||||
void _tabChangesListener() {
|
||||
setState(() => _currentTab = _tabController.index);
|
||||
}
|
||||
|
||||
void _scrollExtentListener() {
|
||||
if (pagingScrollController.position.pixels >
|
||||
MediaQuery.of(context).size.height) {
|
||||
if (!_showBackToTopButton) {
|
||||
setState(() => _showBackToTopButton = true);
|
||||
}
|
||||
} else {
|
||||
if (_showBackToTopButton) {
|
||||
setState(() => _showBackToTopButton = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -145,186 +173,100 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||
// and redirects it to the SliverOverlapInjector below. If it is
|
||||
// missing, then it is possible for the nested "inner" scroll view
|
||||
// below to end up under the SliverAppBar even when the inner
|
||||
// scroll view thinks it has not been scrolled.
|
||||
// This is not necessary if the "headerSliverBuilder" only builds
|
||||
// widgets that do not overlap the next sliver.
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||
context,
|
||||
),
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isNotEmpty) {
|
||||
return SliverAppBar(
|
||||
floating: false,
|
||||
pinned: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => context
|
||||
.read<DocumentsCubit>()
|
||||
.resetSelection(),
|
||||
),
|
||||
title: Text(
|
||||
"${state.selection.length} ${S.of(context).documentsSelectedText}",
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(state),
|
||||
child: Stack(
|
||||
children: [
|
||||
NestedScrollView(
|
||||
key: _nestedScrollViewKey,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||
// and redirects it to the SliverOverlapInjector below. If it is
|
||||
// missing, then it is possible for the nested "inner" scroll view
|
||||
// below to end up under the SliverAppBar even when the inner
|
||||
// scroll view thinks it has not been scrolled.
|
||||
// This is not necessary if the "headerSliverBuilder" only builds
|
||||
// widgets that do not overlap the next sliver.
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||
context,
|
||||
),
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isNotEmpty) {
|
||||
return SliverAppBar(
|
||||
floating: false,
|
||||
pinned: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => context
|
||||
.read<DocumentsCubit>()
|
||||
.resetSelection(),
|
||||
),
|
||||
title: Text(
|
||||
"${state.selection.length} ${S.of(context).documentsSelectedText}",
|
||||
),
|
||||
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(
|
||||
hintText: S.of(context).documentSearchSearchDocuments,
|
||||
onOpenSearch: showDocumentSearchPage,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(text: S.of(context).documentsPageTitle),
|
||||
Tab(text: S.of(context).savedViewsLabel),
|
||||
],
|
||||
),
|
||||
);
|
||||
final desiredTab =
|
||||
(metrics.pixels / metrics.maxScrollExtent).round();
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentTab != desiredTab) {
|
||||
setState(() => _currentTab = desiredTab);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
body: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final metrics = notification.metrics;
|
||||
if (metrics.maxScrollExtent == 0) {
|
||||
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(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: TabBarView(
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildSavedViewsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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() {
|
||||
return SliverToBoxAdapter(
|
||||
child: Row(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_grid_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
|
||||
abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||
@@ -41,6 +43,24 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||
required this.hasLoaded,
|
||||
this.enableHeroAnimation = true,
|
||||
});
|
||||
|
||||
AdaptiveDocumentsView.fromPagedState(
|
||||
DocumentPagingState state, {
|
||||
super.key,
|
||||
this.onSelected,
|
||||
this.onTap,
|
||||
this.onCorrespondentSelected,
|
||||
this.onDocumentTypeSelected,
|
||||
this.onStoragePathSelected,
|
||||
this.onTagSelected,
|
||||
this.isLabelClickable = true,
|
||||
this.enableHeroAnimation = true,
|
||||
required this.hasInternetConnection,
|
||||
this.viewType = ViewType.list,
|
||||
this.selectedDocumentIds = const [],
|
||||
}) : documents = state.documents,
|
||||
isLoading = state.isLoading,
|
||||
hasLoaded = state.hasLoaded;
|
||||
}
|
||||
|
||||
class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
@@ -69,6 +89,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
return _buildGridView();
|
||||
case ViewType.list:
|
||||
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() {
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentGridLoadingWidget.sliver();
|
||||
return const DocumentGridLoadingWidget.sliver();
|
||||
}
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
@@ -161,6 +213,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
return _buildGridView();
|
||||
case ViewType.list:
|
||||
return _buildListView();
|
||||
case ViewType.detailed:
|
||||
return _buildFullView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +224,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
controller: scrollController,
|
||||
primary: false,
|
||||
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() {
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentGridLoadingWidget();
|
||||
}
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
controller: scrollController,
|
||||
primary: false,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
|
||||
@@ -2,51 +2,59 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentPreview extends StatelessWidget {
|
||||
final int id;
|
||||
final DocumentModel document;
|
||||
final BoxFit fit;
|
||||
final Alignment alignment;
|
||||
final double borderRadius;
|
||||
final bool enableHero;
|
||||
final double scale;
|
||||
|
||||
const DocumentPreview({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.document,
|
||||
this.fit = BoxFit.cover,
|
||||
this.alignment = Alignment.center,
|
||||
this.borderRadius = 8.0,
|
||||
this.alignment = Alignment.topCenter,
|
||||
this.borderRadius = 12.0,
|
||||
this.enableHero = true,
|
||||
this.scale = 1.1,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!enableHero) {
|
||||
return _buildPreview(context);
|
||||
}
|
||||
return Hero(
|
||||
tag: "thumb_$id",
|
||||
child: _buildPreview(context),
|
||||
return HeroMode(
|
||||
enabled: enableHero,
|
||||
child: Hero(
|
||||
tag: "thumb_${document.id}",
|
||||
child: _buildPreview(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ClipRRect _buildPreview(BuildContext context) {
|
||||
Widget _buildPreview(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: CachedNetworkImage(
|
||||
fit: fit,
|
||||
alignment: Alignment.topCenter,
|
||||
cacheKey: "thumb_$id",
|
||||
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(id),
|
||||
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),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: CachedNetworkImage(
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
cacheKey: "thumb_${document.id}",
|
||||
imageUrl: context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.getThumbnailUrl(document.id),
|
||||
errorWidget: (ctxt, msg, __) => Text(msg),
|
||||
placeholder: (context, value) => Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
child: AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 1.0,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
borderRadius: 12.0,
|
||||
enableHero: enableHeroAnimation,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 1.0,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _onTap,
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: DocumentPreview(
|
||||
document: document,
|
||||
borderRadius: 12.0,
|
||||
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,
|
||||
child: GestureDetector(
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
document: document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: enableHeroAnimation,
|
||||
|
||||
+18
-25
@@ -1,24 +1,15 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentGridLoadingWidget extends StatelessWidget
|
||||
with DocumentItemPlaceholder {
|
||||
class DocumentGridLoadingWidget extends StatelessWidget {
|
||||
final bool _isSliver;
|
||||
@override
|
||||
final Random random = Random(1257195195);
|
||||
DocumentGridLoadingWidget({super.key}) : _isSliver = false;
|
||||
const DocumentGridLoadingWidget({super.key}) : _isSliver = false;
|
||||
|
||||
DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
const DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -41,8 +32,6 @@ class DocumentGridLoadingWidget extends StatelessWidget
|
||||
}
|
||||
|
||||
Widget _buildPlaceholderGridItem(BuildContext context) {
|
||||
final values = nextValues;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
@@ -68,21 +57,25 @@ class DocumentGridLoadingWidget extends StatelessWidget
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextPlaceholder(
|
||||
length: values.correspondentLength,
|
||||
const TextPlaceholder(
|
||||
length: 70,
|
||||
fontSize: 16,
|
||||
).padded(1),
|
||||
const TextPlaceholder(
|
||||
length: 50,
|
||||
fontSize: 16,
|
||||
).padded(1),
|
||||
TextPlaceholder(
|
||||
length: values.titleLength,
|
||||
fontSize: 16,
|
||||
length: 200,
|
||||
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(),
|
||||
TextPlaceholder(
|
||||
length: 100,
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
mixin DocumentItemPlaceholder {
|
||||
static const _tags = [" ", " ", " "];
|
||||
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
||||
static const _correspondentLengths = <double>[120.0, 80.0, 40.0];
|
||||
|
||||
Random get random;
|
||||
|
||||
RandomDocumentItemPlaceholderValues get nextValues {
|
||||
return RandomDocumentItemPlaceholderValues(
|
||||
tagCount: random.nextInt(_tags.length + 1),
|
||||
correspondentLength: _correspondentLengths[
|
||||
random.nextInt(_correspondentLengths.length - 1)],
|
||||
titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RandomDocumentItemPlaceholderValues {
|
||||
final int tagCount;
|
||||
final double correspondentLength;
|
||||
final double titleLength;
|
||||
|
||||
RandomDocumentItemPlaceholderValues({
|
||||
required this.tagCount,
|
||||
required this.correspondentLength,
|
||||
required this.titleLength,
|
||||
});
|
||||
}
|
||||
+16
-23
@@ -1,21 +1,13 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
||||
|
||||
class DocumentsListLoadingWidget extends StatelessWidget
|
||||
with DocumentItemPlaceholder {
|
||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
final bool _isSliver;
|
||||
DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||
const DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||
|
||||
DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
final Random random = Random(1209571050);
|
||||
const DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -35,26 +27,31 @@ class DocumentsListLoadingWidget extends StatelessWidget
|
||||
|
||||
Widget _buildFakeListItem(BuildContext context) {
|
||||
const fontSize = 14.0;
|
||||
final values = nextValues;
|
||||
return ShimmerPlaceholder(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
dense: true,
|
||||
isThreeLine: true,
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: double.infinity,
|
||||
width: 35,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextPlaceholder(
|
||||
length: values.correspondentLength,
|
||||
const TextPlaceholder(
|
||||
length: 120,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
TextPlaceholder(
|
||||
length: 220,
|
||||
fontSize: Theme.of(context).textTheme.titleMedium!.fontSize!,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
@@ -63,14 +60,10 @@ class DocumentsListLoadingWidget extends StatelessWidget
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
TagsPlaceholder(count: 2, dense: true),
|
||||
SizedBox(height: 2),
|
||||
TextPlaceholder(
|
||||
length: values.titleLength,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
if (values.tagCount > 0)
|
||||
TagsPlaceholder(count: values.tagCount, dense: true),
|
||||
TextPlaceholder(
|
||||
length: 100,
|
||||
length: 250,
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,
|
||||
),
|
||||
],
|
||||
@@ -15,6 +15,7 @@ class TagsPlaceholder extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: count,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => FilterChip(
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
|
||||
class TextPlaceholder extends StatelessWidget {
|
||||
final double length;
|
||||
|
||||
@@ -14,13 +14,65 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final next = viewType.toggle();
|
||||
final icon = next == ViewType.grid ? Icons.grid_view_rounded : Icons.list;
|
||||
return IconButton(
|
||||
late final IconData icon;
|
||||
switch (viewType) {
|
||||
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),
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
|
||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||
@@ -63,7 +62,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
context.read(),
|
||||
);
|
||||
_listenToInboxChanges();
|
||||
context.read<ConnectivityCubit>().reload();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_listenForReceivedFiles();
|
||||
});
|
||||
@@ -82,12 +81,22 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && !_inboxTimer.isActive) {
|
||||
log('App is now in foreground, start polling for statistics.');
|
||||
_listenToInboxChanges();
|
||||
} else if (state != AppLifecycleState.resumed) {
|
||||
log('App is now in background, stop polling for statistics.');
|
||||
_inboxTimer.cancel();
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
log('App is now in foreground');
|
||||
context.read<ConnectivityCubit>().reload();
|
||||
log("Reloaded device connectivity state");
|
||||
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(),
|
||||
),
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
// We need to manually downcast the inboxcubit to the
|
||||
// mixed-in DocumentPagingBlocMixin to use the
|
||||
// DocumentPagingViewMixin in the inbox.
|
||||
BlocProvider<DocumentPagingBlocMixin>.value(
|
||||
value: _inboxCubit,
|
||||
),
|
||||
BlocProvider<InboxCubit>.value(
|
||||
value: _inboxCubit,
|
||||
),
|
||||
],
|
||||
BlocProvider<InboxCubit>.value(
|
||||
value: _inboxCubit,
|
||||
child: const InboxPage(),
|
||||
),
|
||||
// const SettingsPage(),
|
||||
];
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
|
||||
@@ -5,10 +5,10 @@ import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart';
|
||||
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
|
||||
@@ -24,7 +24,8 @@ class InboxPage extends StatefulWidget {
|
||||
State<InboxPage> createState() => _InboxPageState();
|
||||
}
|
||||
|
||||
class _InboxPageState extends State<InboxPage> with DocumentPagingViewMixin {
|
||||
class _InboxPageState extends State<InboxPage>
|
||||
with DocumentPagingViewMixin<InboxPage, InboxCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@@ -54,7 +54,7 @@ class _InboxItemState extends State<InboxItem> {
|
||||
AspectRatio(
|
||||
aspectRatio: InboxItem._a4AspectRatio,
|
||||
child: DocumentPreview(
|
||||
id: widget.document.id,
|
||||
document: widget.document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: false,
|
||||
|
||||
@@ -31,6 +31,8 @@ class DocumentTypeWidget extends StatelessWidget {
|
||||
state.labels[documentTypeId]?.toString() ?? "-",
|
||||
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ class LinkedDocumentsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
|
||||
with DocumentPagingViewMixin {
|
||||
with DocumentPagingViewMixin<LinkedDocumentsPage, LinkedDocumentsCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||
|
||||
mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> {
|
||||
mixin DocumentPagingViewMixin<T extends StatefulWidget,
|
||||
Bloc extends DocumentPagingBlocMixin> on State<T> {
|
||||
ScrollController get pagingScrollController;
|
||||
|
||||
@override
|
||||
@@ -20,7 +20,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget> on State<T> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
DocumentPagingBlocMixin get _bloc => context.read<DocumentPagingBlocMixin>();
|
||||
DocumentPagingBlocMixin get _bloc => context.read<Bloc>();
|
||||
|
||||
void shouldLoadMoreDocumentsListener() async {
|
||||
if (shouldLoadMoreDocuments) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
||||
@@ -12,50 +13,55 @@ class SavedViewList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final savedViewCubit = context.read<SavedViewCubit>();
|
||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
if (state.value.isEmpty) {
|
||||
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),
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivity) {
|
||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
if (state.value.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: HintCard(
|
||||
hintText: S.of(context).savedViewsEmptyStateText,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SavedViewDetailsCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
savedView: view,
|
||||
);
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final view = state.value.values.elementAt(index);
|
||||
return ListTile(
|
||||
enabled: connectivity.isConnected,
|
||||
title: Text(view.name),
|
||||
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>
|
||||
with DocumentPagingViewMixin {
|
||||
with DocumentPagingViewMixin<SavedViewDetailsPage, SavedViewDetailsCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
|
||||
@@ -56,7 +56,7 @@ class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
||||
onChanged: cubit.setViewType,
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||
@@ -29,13 +32,13 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
snap: true,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
title: SearchBar(
|
||||
height: kToolbarHeight - 8,
|
||||
height: kToolbarHeight - 12,
|
||||
supportingText: widget.hintText,
|
||||
onTap: () => widget.onOpenSearch(context),
|
||||
leadingIcon: IconButton(
|
||||
@@ -43,17 +46,22 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
||||
onPressed: Scaffold.of(context).openDrawer,
|
||||
),
|
||||
trailingIcon: IconButton(
|
||||
icon: const CircleAvatar(
|
||||
child: Text("A"),
|
||||
icon: BlocBuilder<PaperlessServerInformationCubit,
|
||||
PaperlessServerInformationState>(
|
||||
builder: (context, state) {
|
||||
return CircleAvatar(
|
||||
child: Text(state.information?.userInitials ?? ''),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AccountSettingsDialog(),
|
||||
builder: (context) => const AccountSettingsDialog(),
|
||||
);
|
||||
},
|
||||
),
|
||||
).paddedOnly(top: 4, bottom: 4),
|
||||
).paddedOnly(top: 8, bottom: 4),
|
||||
bottom: widget.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
enum ViewType {
|
||||
grid,
|
||||
list;
|
||||
list,
|
||||
detailed;
|
||||
|
||||
ViewType toggle() {
|
||||
return this == grid ? list : grid;
|
||||
return ViewType.values[(index + 1) % ViewType.values.length];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,7 @@ class AccountSettingsDialog extends StatelessWidget {
|
||||
children: [
|
||||
ExpansionTile(
|
||||
leading: CircleAvatar(
|
||||
child: Text(state.information?.username
|
||||
?.toUpperCase()
|
||||
.substring(0, 1) ??
|
||||
''),
|
||||
child: Text(state.information?.userInitials ?? ''),
|
||||
),
|
||||
title: Text(state.information?.username ?? ''),
|
||||
subtitle: Text(state.information?.host ?? ''),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
|
||||
@@ -17,7 +18,7 @@ class SimilarDocumentsView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
with DocumentPagingViewMixin {
|
||||
with DocumentPagingViewMixin<SimilarDocumentsView, SimilarDocumentsCubit> {
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
|
||||
@@ -33,44 +34,50 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
|
||||
return DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter(
|
||||
filter: DocumentFilter.initial.copyWith(
|
||||
moreLike: () =>
|
||||
context.read<SimilarDocumentsCubit>().documentId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivity) {
|
||||
return CustomScrollView(
|
||||
controller: pagingScrollController,
|
||||
slivers: [
|
||||
SliverAdaptiveDocumentsView(
|
||||
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,
|
||||
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) =>
|
||||
context.read<SimilarDocumentsCubit>().initialize(),
|
||||
builder: (context, connectivity) {
|
||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (!connectivity.isConnected && !state.hasLoaded) {
|
||||
return const OfflineWidget();
|
||||
}
|
||||
if (state.hasLoaded &&
|
||||
!state.isLoading &&
|
||||
state.documents.isEmpty) {
|
||||
return DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () => context
|
||||
.read<SimilarDocumentsCubit>()
|
||||
.updateFilter(
|
||||
filter: DocumentFilter.initial.copyWith(
|
||||
moreLike: () =>
|
||||
context.read<SimilarDocumentsCubit>().documentId,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
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? host;
|
||||
|
||||
String? get userInitials {
|
||||
return username?.substring(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
PaperlessServerInformationModel({
|
||||
this.host,
|
||||
this.username,
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
|
||||
} on DioError catch (error) {
|
||||
if (error.error is PaperlessServerException ||
|
||||
error.error is Map<String, String>) {
|
||||
throw error.error;
|
||||
throw error.error as Map<String, String>;
|
||||
} else {
|
||||
throw PaperlessServerException(
|
||||
ErrorCode.authenticationFailed,
|
||||
|
||||
+12
-16
@@ -2,14 +2,10 @@ import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_api/src/constants.dart';
|
||||
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
|
||||
|
||||
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
static const _dateTimeConverter = LocalDateTimeJsonConverter();
|
||||
|
||||
final Dio client;
|
||||
|
||||
PaperlessDocumentsApiImpl(this.client);
|
||||
@@ -65,7 +61,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
);
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +78,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
throw const PaperlessServerException(ErrorCode.documentUpdateFailed);
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +105,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
throw const PaperlessServerException(ErrorCode.documentLoadFailed);
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +119,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
}
|
||||
throw const PaperlessServerException(ErrorCode.documentDeleteFailed);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +146,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
}
|
||||
throw const PaperlessServerException(ErrorCode.documentPreviewFailed);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +168,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
} on PaperlessServerException {
|
||||
throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +187,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
);
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +204,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
);
|
||||
return response.data;
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +218,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
response.data as Map<String, dynamic>,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +237,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
}
|
||||
throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +252,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
}
|
||||
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +266,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
return null;
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
|
||||
}
|
||||
throw const PaperlessServerException(ErrorCode.unknown);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Future<T> getSingleResult<T>(
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +66,13 @@ Future<List<T>> getCollection<T>(
|
||||
httpStatusCode: response.statusCode,
|
||||
);
|
||||
} on DioError catch (err) {
|
||||
throw err.error;
|
||||
throw err.error!;
|
||||
}
|
||||
}
|
||||
|
||||
List<T> _collectionFromJson<T>(
|
||||
_CollectionFromJsonSerializationParams<T> params) {
|
||||
_CollectionFromJsonSerializationParams<T> params,
|
||||
) {
|
||||
return params.list.map<T>((result) => params.fromJson(result)).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ dependencies:
|
||||
http: ^0.13.5
|
||||
json_annotation: ^4.7.0
|
||||
intl: ^0.17.0
|
||||
dio: ^4.0.6
|
||||
dio: ^5.0.0
|
||||
collection: ^1.17.0
|
||||
jiffy: ^5.0.0
|
||||
|
||||
|
||||
+38
-70
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
|
||||
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -101,18 +101,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a
|
||||
sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
version: "8.1.1"
|
||||
bloc_test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: bloc_test
|
||||
sha256: "622b97678bf8c06a94f4c26a89ee9ebf7319bf775383dee2233e86e1f94ee28d"
|
||||
sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.0"
|
||||
version: "9.1.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -253,50 +253,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "3f8fe4e504c2d33696dac671a54909743bc6a902a9bb0902306f7a2aed7e528e"
|
||||
sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.9"
|
||||
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"
|
||||
version: "3.0.3"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: b8795b9238bf83b64375f63492034cb3d8e222af4d9ce59dda085edf038fa06f
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
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"
|
||||
version: "1.2.4"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -309,10 +277,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040"
|
||||
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.2"
|
||||
version: "1.6.3"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -341,18 +309,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: dart_code_metrics
|
||||
sha256: bb4ec5e729788dde5f7e8e9df4c05ec3b78532a5763e635337153ce40085514b
|
||||
sha256: "026e28da197a03caeccccc0b174ec98ef03da3c81c4543314d7add121aab4375"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.1"
|
||||
version: "5.6.0"
|
||||
dart_code_metrics_presets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_code_metrics_presets
|
||||
sha256: "43dc1fdcb424fc3aa79964304d09eeda4f199351c52cdc854f8228a9d0296b60"
|
||||
sha256: "9c51724f836aebc4465228954cb5757e5a99737af26a452b5dec0a2d5d0b4d66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -437,10 +405,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
|
||||
sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
version: "5.0.0"
|
||||
dots_indicator:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -547,10 +515,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557"
|
||||
sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.1"
|
||||
version: "8.1.2"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -735,18 +703,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "7cc92eabe01e3f1babe1571c5560b135dfc762a34e41e9056881e2196b178ec1"
|
||||
sha256: "774fa28b07f3a82c93596bc137be33189fec578ed3447a93a5a11c93435de394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.2"
|
||||
version: "8.1.3"
|
||||
font_awesome_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc"
|
||||
sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.0"
|
||||
version: "10.4.0"
|
||||
form_builder_validators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -836,10 +804,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hydrated_bloc
|
||||
sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7"
|
||||
sha256: eb92d88061b6b911c48779b08a91c8a9f3a3aa8475f80d9380045375d9876536
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.1.0"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -937,10 +905,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: ba48fe0e1cae140a0813ce68c2540250d7f573a8ae4d4b6c681b2d2583584953
|
||||
sha256: cfcbc4936e288d61ef85a04feef6b95f49ba496d4fd98364e6abafb462b06a1f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.17"
|
||||
version: "1.0.18"
|
||||
local_auth_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1176,10 +1144,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
|
||||
sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.1.8"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1312,10 +1280,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pretty_dio_logger
|
||||
sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544"
|
||||
sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0-beta-1"
|
||||
version: "1.3.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1392,10 +1360,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e
|
||||
sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1685,10 +1653,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809"
|
||||
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.8"
|
||||
version: "6.1.9"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1701,10 +1669,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3
|
||||
sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.18"
|
||||
version: "6.1.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ dependencies:
|
||||
package_info_plus: ^1.4.3+1
|
||||
font_awesome_flutter: ^10.1.0
|
||||
local_auth: ^2.1.2
|
||||
connectivity_plus: ^2.3.9
|
||||
connectivity_plus: ^3.0.3
|
||||
|
||||
flutter_native_splash: ^2.2.11
|
||||
share_plus: ^6.2.0
|
||||
@@ -77,7 +77,7 @@ dependencies:
|
||||
badges: ^2.0.3
|
||||
flutter_colorpicker: ^1.0.3
|
||||
provider: ^6.0.5
|
||||
dio: ^4.0.6
|
||||
dio: ^5.0.0
|
||||
hydrated_bloc: ^9.0.0
|
||||
json_annotation: ^4.7.0
|
||||
pretty_dio_logger: ^1.2.0-beta-1
|
||||
|
||||
Executable → Regular
+1
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
pushd ../
|
||||
pushd packages/paperless_api
|
||||
flutter pub get
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
Reference in New Issue
Block a user