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

Feature: Detailed view type
This commit is contained in:
Anton Stubenbord
2023-02-14 23:56:14 +01:00
committed by GitHub
44 changed files with 1028 additions and 726 deletions
+4 -9
View File
@@ -75,22 +75,17 @@ To get a local copy up and running follow these simple steps.
### Prerequisites
* 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,
));
}
+13 -9
View File
@@ -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;
}
+17 -6
View File
@@ -8,18 +8,22 @@ extension WidgetPadding on Widget {
);
}
Widget paddedSymmetrically({double horizontal = 0.0, double vertical = 0.0}) {
Widget paddedSymmetrically({
double horizontal = 0.0,
double vertical = 0.0,
}) {
return Padding(
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,
@@ -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,
});
}
@@ -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,
),
);
}
}
+19 -21
View File
@@ -25,7 +25,6 @@ import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/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,
);
}
+3 -2
View File
@@ -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,
@@ -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,
@@ -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!;
}
}
}
@@ -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();
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -1,8 +1,9 @@
#!/bin/bash
pushd ../
pushd packages/paperless_api
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
popd
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
flutter pub run intl_utils:generate
flutter pub run intl_utils:generate