Merge branch 'main' into download_to_public

This commit is contained in:
Iulian Ciorascu
2023-02-11 20:10:34 +01:00
115 changed files with 1190 additions and 1342 deletions

View File

@@ -79,20 +79,34 @@ To get a local copy up and running follow these simple steps.
* Install an IDE of your choice (e.g. VSCode with the Dart/Flutter extensions)
### Install dependencies and generate files
1. Clone the repo
1. First, clone the repository:
```sh
git clone https://github.com/astubenbord/paperless-mobile.git
```
2. Install the dependencies
You can now either run the `install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate code for both the app and subpackages, or you can manually run the following steps:
#### Inside the `packages/paperless_api/` folder:
2. Install the dependencies for `paperless_api`
```sh
flutter pub get
```
3. Build generated files (for json_serializable etc.)
3. Build generated files for `paperless_api`
```sh
flutter pub run build_runner build --delete-conflicting-outputs
```
#### Inside the project's root folder
4. Install the dependencies for the app
```sh
flutter pub get
```
5. Build generated files for the app
```sh
flutter packages pub run build_runner build --delete-conflicting-outputs
```
4. Generate the localization files
6. Generate the localization files for the app
```sh
flutter pub run intl_utils:generate
```
@@ -117,14 +131,18 @@ buildTypes {
```
2. Build the app with release profile (here for android):
```sh
flutter build apk --split-per-abi
flutter build apk
```
(the --release flag is implicit for the build command)
The --release flag is implicit for the build command. You can also run this command with --split-per-abi, which will generate three separate (smaller) binaries.
3. Install the app to your device
3. Install the app to your device (when omitting the `--split-per-abi` flag)
```sh
flutter install
```
or when you built with `--split-per-abi`
```sh
flutter install --use-application-binary=build/pp/outputs/flutter-apk/<apk_file_name>.apk
```
## Languages and Translations
If you want to contribute to translate the app into your language, create a new <a href="https://github.com/astubenbord/paperless-mobile/discussions/categories/languages-and-translations">Discussion</a> and you will be invited to the <a href="https://localizely.com/">Localizely</a> project.
@@ -138,17 +156,13 @@ This project is registered as an open source project in Localizely, which offers
<!-- ROADMAP -->
## Roadmap
- [x] Add download functionality (implemented, but flutter cannot download to useful directories except app directory)
- [x] Add document share support
- [x] Improvements to UX (e.g. form fields show clear button while empty)
- [ ] Fully custom document scanner optimized for common white A4 documents and optimized for the use with Paperless
- [ ] Add more languages
- [ ] Support for IOS
- [ ] Support for IOS and publish to AppStore
- [ ] Automatic releases and CI/CD with fastlane
- [ ] Templates for recurring scans (e.g. monthly payrolls with same title, dates at end of month, fixed correspondent and document type)
- [ ] Custom document scanner optimized for common white A4 documents (currently using edge_detection, which is okay but not optimal for this use case)
- [ ] Support multiple instances (low prio)
See the [open issues](https://github.com/astubenbord/paperless-mobile/issues) for a full list of proposed features (and known issues).
See the [open issues](https://github.com/astubenbord/paperless-mobile/issues) for a full list of issues and [open feature requests](https://github.com/astubenbord/paperless-mobile/discussions/categories/feature-requests) for requested features.
<!-- CONTRIBUTING -->
## Contributing
@@ -173,35 +187,6 @@ I do this in my free time, so if you like the project, consider buying me a coff
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/astubenbord)
<!-- USAGE EXAMPLES -->
## Screenshots
Here are some impressions from the app!
#### Login Page
<img src="https://user-images.githubusercontent.com/79228196/198392006-f3badfb3-17c7-4b46-91c7-595c93b146b7.png" width=200/> <img src="https://user-images.githubusercontent.com/79228196/198392041-1ef5de1e-7d26-47f6-bdfb-f5ac831ddb30.png" width=200/>
#### Documents Overview (List)
<img src="https://user-images.githubusercontent.com/79228196/198392750-a0e4c0b1-9c1c-4346-980a-64d1cc192a99.png" width=200> <img src="https://user-images.githubusercontent.com/79228196/198392767-995536e4-5737-476a-9e78-34c37fac9c60.png" width=200>
#### Documents Overview (Grid)
<img src="https://user-images.githubusercontent.com/79228196/198393000-83a32969-c0d8-4f81-bb20-8afc79057d62.png" width=200> <img src="https://user-images.githubusercontent.com/79228196/198393018-2f1d02fc-a410-45d8-a022-32c0ae377717.png" width=200>
#### Document Filter/Search (More filters below!)
<img src="https://user-images.githubusercontent.com/79228196/198393168-60aa5114-85a8-4def-9ca9-5374e0b92aef.png" width=200> <img src="https://user-images.githubusercontent.com/79228196/198393173-db38e99e-f408-4a31-bc6a-fcce91a2a900.png" width=200>
#### Document Details
<img src="https://user-images.githubusercontent.com/79228196/198393856-6b11dbdc-77ce-44e8-a69c-b0a2536cd38b.png" width=200> <img src="https://user-images.githubusercontent.com/79228196/198393867-39e2148e-53a7-4fc9-8b6d-2ab038dfea64.png" width=200>
#### Edit Document
<img src="https://user-images.githubusercontent.com/79228196/198393926-1adc3fe8-6981-4b20-854e-6d17611a1d7a.png" width=200><img src="https://user-images.githubusercontent.com/79228196/198393931-c3b214db-e96e-4da4-8327-9c4779c2c64a.png" width=200>
#### Scan
<img src="https://user-images.githubusercontent.com/79228196/198394782-0955a57b-90c6-4c42-946c-ecf5f94bf704.png" width=200><img src="https://user-images.githubusercontent.com/79228196/198394796-cc7a5bb3-81b4-4010-9444-33440eb9aef7.png" width=200>
#### Upload
<img src="https://user-images.githubusercontent.com/79228196/198394876-7438dcfe-d901-4ac8-8e7f-0eba7c72a5d7.png" width=200><img src="https://user-images.githubusercontent.com/79228196/198394883-2721211b-17dc-405b-9ee9-2ca943e630fa.png" width=200>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/astubenbord/paperless-mobile.svg?style=for-the-badge
@@ -229,4 +214,4 @@ Made with [contrib.rocks](https://contrib.rocks).
## Troubleshooting
#### Suggestions are not selectable in any of the label form fields
This is a known issue and it has to do with accessibility features of Android. Password managers such as Bitwarden often caused this issue to occur. Luckily, this can be resolved by turning off the accessibility features in these apps.
This is a known issue and it has to do with accessibility features of Android. Password managers such as Bitwarden often caused this issue. Luckily, this can be resolved by turning off the accessibility features in these apps. This could also be observed with apps that are allowed to display over other apps, such as emulations of the dynamic island on android.

View File

View File

@@ -0,0 +1,15 @@
With this app you can conveniently add, manage or simply find documents stored in your paperless server without any compromises.
🚀 Features
✔️ View your documents at a glance, in a compact list or a more detailed grid view
✔️ Add, delete or edit your documents
✔️ Share, download and preview PDF files
✔️ Manage and assign correspondents, document types, tags and storage paths
✔️ Scan and upload documents to paperless with preset correspondent, document type, tags and creation date
✔️ Upload existing documents from other apps via Paperless Mobile
✔️ See all new documents in a dedicated inbox
✔️ Search for documents using a wide range of filter criteria
✔️ Secure your data with biometric authentication across sessions
✔️ Support for TLS mutual authentication (client certificates)
✔️ Modern, intuitive UI built according to the Material Design 3 specification
✔️ Available in English and German language (more to come!)

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -0,0 +1 @@
An (almost) fully fledged Paperless mobile client.

View File

@@ -0,0 +1 @@
Paperless Mobile

8
install_dependencies.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
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

View File

@@ -37,5 +37,16 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
]
end
# End of the permission_handler configuration
end
end

View File

@@ -198,6 +198,6 @@ SPEC CHECKSUMS:
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
COCOAPODS: 1.11.3

View File

@@ -2,24 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia</string>
</array>
</dict>
<dict/>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>To upload photos, please allow permission to access your photo library.</string>
<key>NSFaceIDUsageDescription</key>
<string>Why is my app authenticating using face id?</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -38,14 +22,34 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia</string>
</array>
</dict>
<dict/>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>Allow this app to use FaceID to secure your login credentials.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow this app to access your photo library to upload images from this device.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -61,12 +65,8 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarHidden</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Allow this app access to your camera to scan documents.</string>
<key>UISupportsDocumentBrowser</key>
<true/>
</dict>

View File

@@ -1,6 +1,6 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
extension AddressableHydratedStorage on Storage {
ApplicationSettingsState get settings {

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:url_launcher/link.dart';

View File

@@ -7,6 +7,8 @@ import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
part 'document_details_state.dart';
@@ -73,6 +75,24 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
emit(state.copyWith(document: document));
}
Future<void> shareDocument() async {
final documentBytes = await _api.download(state.document);
final dir = await getTemporaryDirectory();
final String path = "${dir.path}/${state.document.originalFileName}";
await File(path).writeAsBytes(documentBytes);
Share.shareXFiles(
[
XFile(
path,
name: state.document.originalFileName,
mimeType: "application/pdf",
lastModified: state.document.modified,
),
],
subject: state.document.title,
);
}
@override
Future<void> close() {
for (final element in _subscriptions) {

View File

@@ -4,31 +4,27 @@ import 'package:badges/badges.dart' as b;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
//TODO: Refactor this into several widgets
class DocumentDetailsPage extends StatefulWidget {
@@ -49,7 +45,7 @@ class DocumentDetailsPage extends StatefulWidget {
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
late Future<DocumentMetaData> _metaData;
static const double _itemPadding = 24;
@override
void initState() {
super.initState();
@@ -70,7 +66,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
length: 4,
child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: widget.allowEdit ? _buildAppBar() : null,
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
@@ -98,7 +94,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
.onPrimaryContainer,
),
),
),
Tab(
@@ -107,7 +104,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
.onPrimaryContainer,
),
),
),
Tab(
@@ -116,7 +114,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
.onPrimaryContainer,
),
),
),
Tab(
@@ -146,20 +145,26 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
child: TabBarView(
children: [
_buildDocumentOverview(
state.document,
DocumentOverviewWidget(
document: state.document,
itemSpacing: _itemPadding,
queryString: widget.titleAndContentQueryString,
),
_buildDocumentContentView(
state.document,
state,
DocumentContentWidget(
isFullContentLoaded: state.isFullContentLoaded,
document: state.document,
fullContent: state.fullContent,
queryString: widget.titleAndContentQueryString,
),
_buildDocumentMetaDataView(
state.document,
DocumentMetaDataWidget(
document: state.document,
itemSpacing: _itemPadding,
metaData: _metaData,
),
const SimilarDocumentsView(),
],
),
).paddedSymmetrically(horizontal: 8);
);
},
),
),
@@ -168,7 +173,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
);
}
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildAppBar() {
Widget _buildEditButton() {
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
final _filteredSuggestions =
@@ -176,7 +181,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
if (!connectivityState.isConnected) {
return Container();
return const SizedBox.shrink();
}
return b.Badge(
position: b.BadgePosition.topEnd(top: -12, end: -6),
@@ -244,8 +249,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
IconButton(
tooltip: S.of(context).documentDetailsPageShareTooltip,
icon: const Icon(Icons.share),
onPressed:
isConnected ? () => _onShare(state.document) : null,
onPressed: isConnected
? () =>
context.read<DocumentDetailsCubit>().shareDocument()
: null,
),
],
);
@@ -265,7 +272,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: EditDocumentCubit(
value: DocumentEditCubit(
document,
documentsApi: context.read(),
correspondentRepository: context.read(),
@@ -279,7 +286,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
value: cubit,
),
],
child: BlocListener<EditDocumentCubit, EditDocumentState>(
child: BlocListener<DocumentEditCubit, DocumentEditState>(
listenWhen: (previous, current) =>
previous.document != current.document,
listener: (context, state) {
@@ -317,203 +324,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}
}
Widget _buildDocumentMetaDataView(DocumentModel document) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, state) {
if (!state.isConnected) {
return const Center(
child: OfflineWidget(),
);
}
return FutureBuilder<DocumentMetaData>(
future: _metaData,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final meta = snapshot.data!;
return ListView(
children: [
_DetailsItem.text(DateFormat().format(document.modified),
label: S.of(context).documentModifiedPropertyLabel,
context: context)
.paddedOnly(bottom: 16),
_DetailsItem.text(DateFormat().format(document.added),
label: S.of(context).documentAddedPropertyLabel,
context: context)
.paddedSymmetrically(vertical: 16),
_DetailsItem(
label: S
.of(context)
.documentArchiveSerialNumberPropertyLongLabel,
content: document.archiveSerialNumber != null
? Text(document.archiveSerialNumber.toString())
: TextButton.icon(
icon: const Icon(Icons.archive_outlined),
label: Text(S
.of(context)
.documentDetailsPageAssignAsnButtonLabel),
onPressed: widget.allowEdit
? () => _assignAsn(document)
: null,
),
).paddedSymmetrically(vertical: 16),
_DetailsItem.text(
meta.mediaFilename,
context: context,
label:
S.of(context).documentMetaDataMediaFilenamePropertyLabel,
).paddedSymmetrically(vertical: 16),
_DetailsItem.text(
meta.originalChecksum,
context: context,
label: S.of(context).documentMetaDataChecksumLabel,
).paddedSymmetrically(vertical: 16),
_DetailsItem.text(formatBytes(meta.originalSize, 2),
label:
S.of(context).documentMetaDataOriginalFileSizeLabel,
context: context)
.paddedSymmetrically(vertical: 16),
_DetailsItem.text(
meta.originalMimeType,
label: S.of(context).documentMetaDataOriginalMimeTypeLabel,
context: context,
).paddedSymmetrically(vertical: 16),
],
);
},
);
},
);
}
Future<void> _assignAsn(DocumentModel document) async {
try {
await context.read<DocumentDetailsCubit>().assignAsn(document);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
Widget _buildDocumentContentView(
DocumentModel document,
DocumentDetailsState state,
) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HighlightedText(
text: (state.isFullContentLoaded
? state.fullContent
: document.content) ??
"",
highlights: widget.titleAndContentQueryString != null
? widget.titleAndContentQueryString!.split(" ")
: [],
style: Theme.of(context).textTheme.bodyMedium,
caseSensitive: false,
),
if (!state.isFullContentLoaded && (document.content ?? '').isNotEmpty)
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
child:
Text(S.of(context).documentDetailsPageLoadFullContentLabel),
onPressed: () {
context.read<DocumentDetailsCubit>().loadFullContent();
},
),
),
],
).padded(8).paddedOnly(top: 14),
);
}
Widget _buildDocumentOverview(DocumentModel document) {
return ListView(
children: [
_DetailsItem(
label: S.of(context).documentTitlePropertyLabel,
content: HighlightedText(
text: document.title,
highlights: widget.titleAndContentQueryString?.split(" ") ?? [],
style: Theme.of(context).textTheme.bodyLarge,
),
).paddedOnly(bottom: 16),
_DetailsItem.text(
DateFormat.yMMMMd().format(document.created),
context: context,
label: S.of(context).documentCreatedPropertyLabel,
).paddedSymmetrically(vertical: 16),
Visibility(
visible: document.documentType != null,
child: _DetailsItem(
label: S.of(context).documentDocumentTypePropertyLabel,
content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.documentType,
),
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.correspondent != null,
child: _DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.correspondent,
),
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.storagePath != null,
child: _DetailsItem(
label: S.of(context).documentStoragePathPropertyLabel,
content: StoragePathWidget(
pathId: document.storagePath,
),
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.tags.isNotEmpty,
child: _DetailsItem(
label: S.of(context).documentTagsPropertyLabel,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: widget.isLabelClickable,
tagIds: document.tags,
),
),
).paddedSymmetrically(vertical: 16),
),
],
);
}
///
/// Downloads file to temporary directory, from which it can then be shared.
///
Future<void> _onShare(DocumentModel document) async {
Uint8List documentBytes =
await context.read<PaperlessDocumentsApi>().download(document);
final dir = await getTemporaryDirectory();
final String path = "${dir.path}/${document.originalFileName}";
await File(path).writeAsBytes(documentBytes);
Share.shareXFiles(
[
XFile(
path,
name: document.originalFileName,
mimeType: "application/pdf",
lastModified: document.modified,
)
],
subject: document.title,
);
}
void _onDelete(DocumentModel document) async {
final delete = await showDialog(
context: context,
@@ -546,42 +356,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}
}
class _DetailsItem extends StatelessWidget {
final String label;
final Widget content;
const _DetailsItem({
Key? key,
required this.label,
required this.content,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
content,
],
),
);
}
_DetailsItem.text(
String text, {
required this.label,
required BuildContext context,
}) : content = Text(
text,
style: Theme.of(context).textTheme.bodyLarge,
);
}
class ColoredTabBar extends Container implements PreferredSizeWidget {
ColoredTabBar({
super.key,

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class DetailsItem extends StatelessWidget {
final String label;
final Widget content;
const DetailsItem({
Key? key,
required this.label,
required this.content,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
content,
],
);
}
DetailsItem.text(
String text, {
required this.label,
required BuildContext context,
}) : content = Text(
text,
style: Theme.of(context).textTheme.bodyLarge,
);
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class DocumentContentWidget extends StatelessWidget {
final bool isFullContentLoaded;
final String? fullContent;
final String? queryString;
final DocumentModel document;
const DocumentContentWidget({
super.key,
required this.isFullContentLoaded,
this.fullContent,
required this.document,
this.queryString,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HighlightedText(
text: (isFullContentLoaded ? fullContent : document.content) ?? "",
highlights: queryString != null ? queryString!.split(" ") : [],
style: Theme.of(context).textTheme.bodyMedium,
caseSensitive: false,
),
if (!isFullContentLoaded && (document.content ?? '').isNotEmpty)
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
child:
Text(S.of(context).documentDetailsPageLoadFullContentLabel),
onPressed: () {
context.read<DocumentDetailsCubit>().loadFullContent();
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
class DocumentMetaDataWidget extends StatelessWidget {
final Future<DocumentMetaData> metaData;
final DocumentModel document;
final double itemSpacing;
const DocumentMetaDataWidget({
super.key,
required this.metaData,
required this.document,
required this.itemSpacing,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, state) {
if (!state.isConnected) {
return const Center(
child: OfflineWidget(),
);
}
return FutureBuilder<DocumentMetaData>(
future: metaData,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final meta = snapshot.data!;
return ListView(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
children: [
DetailsItem(
label: S
.of(context)
.documentArchiveSerialNumberPropertyLongLabel,
content: document.archiveSerialNumber != null
? Text(document.archiveSerialNumber.toString())
: TextButton.icon(
icon: const Icon(Icons.archive_outlined),
label: Text(S
.of(context)
.documentDetailsPageAssignAsnButtonLabel),
onPressed: () => _assignAsn(context),
),
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(DateFormat().format(document.modified),
label: S.of(context).documentModifiedPropertyLabel,
context: context)
.paddedOnly(bottom: itemSpacing),
DetailsItem.text(DateFormat().format(document.added),
label: S.of(context).documentAddedPropertyLabel,
context: context)
.paddedOnly(bottom: itemSpacing),
DetailsItem.text(
meta.mediaFilename,
context: context,
label:
S.of(context).documentMetaDataMediaFilenamePropertyLabel,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
meta.originalChecksum,
context: context,
label: S.of(context).documentMetaDataChecksumLabel,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(formatBytes(meta.originalSize, 2),
label:
S.of(context).documentMetaDataOriginalFileSizeLabel,
context: context)
.paddedOnly(bottom: itemSpacing),
DetailsItem.text(
meta.originalMimeType,
label: S.of(context).documentMetaDataOriginalMimeTypeLabel,
context: context,
).paddedOnly(bottom: itemSpacing),
],
);
},
);
},
);
}
Future<void> _assignAsn(BuildContext context) async {
try {
await context.read<DocumentDetailsCubit>().assignAsn(document);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class DocumentOverviewWidget extends StatelessWidget {
final DocumentModel document;
final String? queryString;
final double itemSpacing;
const DocumentOverviewWidget({
super.key,
required this.document,
this.queryString,
required this.itemSpacing,
});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
children: [
DetailsItem(
label: S.of(context).documentTitlePropertyLabel,
content: HighlightedText(
text: document.title,
highlights: queryString?.split(" ") ?? [],
style: Theme.of(context).textTheme.bodyLarge,
),
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
DateFormat.yMMMMd().format(document.created),
context: context,
label: S.of(context).documentCreatedPropertyLabel,
).paddedOnly(bottom: itemSpacing),
Visibility(
visible: document.documentType != null,
child: DetailsItem(
label: S.of(context).documentDocumentTypePropertyLabel,
content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.documentType,
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.correspondent != null,
child: DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.correspondent,
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.storagePath != null,
child: DetailsItem(
label: S.of(context).documentStoragePathPropertyLabel,
content: StoragePathWidget(
pathId: document.storagePath,
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.tags.isNotEmpty,
child: DetailsItem(
label: S.of(context).documentTagsPropertyLabel,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: false,
tagIds: document.tags,
),
),
).paddedOnly(bottom: itemSpacing),
),
],
);
}
}

View File

@@ -1,19 +1,15 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:collection/collection.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
part 'edit_document_state.dart';
part 'document_edit_state.dart';
class EditDocumentCubit extends Cubit<EditDocumentState> {
class DocumentEditCubit extends Cubit<DocumentEditState> {
final DocumentModel _initialDocument;
final PaperlessDocumentsApi _docsApi;
@@ -24,7 +20,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
final LabelRepository<Tag> _tagRepository;
final List<StreamSubscription> _subscriptions = [];
EditDocumentCubit(
DocumentEditCubit(
DocumentModel document, {
required PaperlessDocumentsApi documentsApi,
required LabelRepository<Correspondent> correspondentRepository,
@@ -40,7 +36,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
_tagRepository = tagRepository,
_notifier = notifier,
super(
EditDocumentState(
DocumentEditState(
document: document,
correspondents: correspondentRepository.current?.values ?? {},
documentTypes: documentTypeRepository.current?.values ?? {},

View File

@@ -1,6 +1,6 @@
part of 'edit_document_cubit.dart';
part of 'document_edit_cubit.dart';
class EditDocumentState extends Equatable {
class DocumentEditState extends Equatable {
final DocumentModel document;
final Map<int, Correspondent> correspondents;
@@ -8,7 +8,7 @@ class EditDocumentState extends Equatable {
final Map<int, StoragePath> storagePaths;
final Map<int, Tag> tags;
const EditDocumentState({
const DocumentEditState({
required this.correspondents,
required this.documentTypes,
required this.storagePaths,
@@ -25,14 +25,14 @@ class EditDocumentState extends Equatable {
document,
];
EditDocumentState copyWith({
DocumentEditState copyWith({
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, StoragePath>? storagePaths,
Map<int, Tag>? tags,
DocumentModel? document,
}) {
return EditDocumentState(
return DocumentEditState(
document: document ?? this.document,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,

View File

@@ -13,7 +13,7 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.dart';
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
@@ -51,12 +51,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
void initState() {
super.initState();
_filteredSuggestions = widget.suggestions
.documentDifference(context.read<EditDocumentCubit>().state.document);
.documentDifference(context.read<DocumentEditCubit>().state.document);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<EditDocumentCubit, EditDocumentState>(
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) {
return Scaffold(
resizeToAvoidBottomInset: false,
@@ -252,7 +252,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
_isSubmitLoading = true;
});
try {
await context.read<EditDocumentCubit>().updateDocument(mergedDocument);
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
showSnackBar(context, S.of(context).documentUpdateSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);

View File

@@ -19,8 +19,8 @@ import 'package:paperless_mobile/features/document_search/view/document_search_p
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/widgets/scanned_image_item.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart';
import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -2,11 +2,17 @@ import 'package:collection/collection.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
part 'document_search_state.dart';
part 'document_search_cubit.g.dart';
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
with PagedDocumentsMixin {
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@override

View File

@@ -1,9 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
part 'document_search_state.g.dart';
part of 'document_search_cubit.dart';
enum SearchView {
suggestions,
@@ -11,7 +6,7 @@ enum SearchView {
}
@JsonSerializable(ignoreUnannotated: true)
class DocumentSearchState extends PagedDocumentsState {
class DocumentSearchState extends DocumentPagingState {
@JsonKey()
final List<String> searchHistory;
final SearchView view;

View File

@@ -1,10 +1,8 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';

View File

@@ -5,11 +5,16 @@ import 'package:flutter/foundation.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
part 'documents_state.dart';
part 'documents_cubit.g.dart';
class DocumentsCubit extends HydratedCubit<DocumentsState>
with PagedDocumentsMixin {
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@@ -94,4 +99,8 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
notifier.unsubscribe(this);
return super.close();
}
void setViewType(ViewType viewType) {
emit(state.copyWith(viewType: viewType));
}
}

View File

@@ -1,13 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
part of 'documents_cubit.dart';
class DocumentsState extends PagedDocumentsState {
@JsonKey(includeFromJson: true, includeToJson: false)
@JsonSerializable()
class DocumentsState extends DocumentPagingState {
@JsonKey(includeFromJson: false, includeToJson: false)
final List<DocumentModel> selection;
final ViewType viewType;
const DocumentsState({
this.selection = const [],
this.viewType = ViewType.list,
super.value = const [],
super.filter = const DocumentFilter(),
super.hasLoaded = false,
@@ -22,6 +24,7 @@ class DocumentsState extends PagedDocumentsState {
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
List<DocumentModel>? selection,
ViewType? viewType,
}) {
return DocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded,
@@ -29,38 +32,22 @@ class DocumentsState extends PagedDocumentsState {
value: value ?? this.value,
filter: filter ?? this.filter,
selection: selection ?? this.selection,
viewType: viewType ?? this.viewType,
);
}
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
_$DocumentsStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
@override
List<Object?> get props => [
selection,
viewType,
...super.props,
];
Map<String, dynamic> toJson() {
final json = {
'hasLoaded': hasLoaded,
'isLoading': isLoading,
'filter': filter.toJson(),
'value':
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
};
return json;
}
factory DocumentsState.fromJson(Map<String, dynamic> json) {
return DocumentsState(
hasLoaded: json['hasLoaded'],
isLoading: json['isLoading'],
value: (json['value'] as List<dynamic>)
.map((e) =>
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
.toList(),
filter: DocumentFilter.fromJson(json['filter']),
);
}
@override
DocumentsState copyWithPaged({
bool? hasLoaded,

View File

@@ -1,7 +1,4 @@
import 'dart:developer';
import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
@@ -9,21 +6,18 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.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';
import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -263,14 +257,6 @@ class _DocumentsPageState extends State<DocumentsPage>
),
_buildViewActions(),
BlocBuilder<DocumentsCubit, DocumentsState>(
// Not required anymore since saved views are now handled separately
// buildWhen: (previous, current) =>
// !const ListEquality().equals(
// previous.documents,
// current.documents,
// ) ||
// previous.selectedIds !=
// current.selectedIds,
builder: (context, state) {
if (state.hasLoaded &&
state.documents.isEmpty) {
@@ -285,13 +271,9 @@ class _DocumentsPageState extends State<DocumentsPage>
),
);
}
return BlocBuilder<
ApplicationSettingsCubit,
ApplicationSettingsState>(
builder: (context, settings) {
return SliverAdaptiveDocumentsView(
viewType:
settings.preferredViewType,
viewType: state.viewType,
onTap: _openDetails,
onSelected: context
.read<DocumentsCubit>()
@@ -309,10 +291,7 @@ class _DocumentsPageState extends State<DocumentsPage>
hasLoaded: state.hasLoaded,
isLabelClickable: true,
isLoading: state.isLoading,
selectedDocumentIds:
state.selectedIds,
);
},
selectedDocumentIds: state.selectedIds,
);
},
),
@@ -360,18 +339,11 @@ class _DocumentsPageState extends State<DocumentsPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SortDocumentsButton(),
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return IconButton(
icon: Icon(
state.preferredViewType == ViewType.list
? Icons.grid_view_rounded
: Icons.list,
),
onPressed: () =>
context.read<ApplicationSettingsCubit>().setViewType(
state.preferredViewType.toggle(),
),
return ViewTypeSelectionWidget(
viewType: state.viewType,
onChanged: context.read<DocumentsCubit>().setViewType,
);
},
)

View File

@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/empty_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class DocumentsEmptyState extends StatelessWidget {
final PagedDocumentsState state;
final DocumentPagingState state;
final VoidCallback? onReset;
const DocumentsEmptyState({
Key? key,

View File

@@ -4,9 +4,8 @@ import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.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/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/document_type_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';

View File

@@ -3,9 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -1,18 +1,10 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
enum DateRangeSelection { before, after }

View File

@@ -4,8 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget {

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_type_ahead.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class BulkDeleteConfirmationDialog extends StatelessWidget {

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
/// Meant to be used with blocbuilder.
class ViewTypeSelectionWidget extends StatelessWidget {
final ViewType viewType;
final void Function(ViewType type) onChanged;
const ViewTypeSelectionWidget({
super.key,
required this.viewType,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final next = viewType.toggle();
final icon = next == ViewType.grid ? Icons.grid_view_rounded : Icons.list;
return IconButton(
icon: Icon(icon),
onPressed: () {
onChanged(next);
},
);
}
}

View File

@@ -2,13 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class SortDocumentsButton extends StatelessWidget {
const SortDocumentsButton({

View File

@@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
class ViewActions extends StatelessWidget {
const ViewActions({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SortDocumentsButton(),
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
final cubit = context.read<ApplicationSettingsCubit>();
switch (settings.preferredViewType) {
case ViewType.grid:
return IconButton(
icon: const Icon(Icons.list),
onPressed: () =>
cubit.setViewType(settings.preferredViewType.toggle()),
);
case ViewType.list:
return IconButton(
icon: const Icon(Icons.grid_view_rounded),
onPressed: () =>
cubit.setViewType(settings.preferredViewType.toggle()),
);
}
},
)
],
);
}
}

View File

@@ -1,10 +1,11 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart';
part 'edit_label_state.dart';
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
final LabelRepository<T> _repository;
@@ -13,7 +14,7 @@ class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
EditLabelCubit(LabelRepository<T> repository)
: _repository = repository,
super(const EditLabelInitial()) {
super(EditLabelState<T>(labels: repository.current?.values ?? {})) {
_subscription = repository.values.listen(
(event) => emit(EditLabelState(labels: event?.values ?? {})),
);

View File

@@ -1,16 +1,10 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
part of 'edit_label_cubit.dart';
@immutable
class EditLabelState<T> extends Equatable {
final Map<int, T> labels;
const EditLabelState({required this.labels});
const EditLabelState({this.labels = const {}});
@override
List<Object> get props => [labels];
}
class EditLabelInitial<T> extends EditLabelState<T> {
const EditLabelInitial() : super(labels: const {});
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -10,25 +12,21 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/route_description.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/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/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -45,14 +43,16 @@ class HomePage extends StatefulWidget {
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0;
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
late final InboxCubit _inboxCubit;
late Timer _inboxTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeData(context);
_inboxCubit = InboxCubit(
context.read(),
@@ -62,12 +62,43 @@ class _HomePageState extends State<HomePage> {
context.read(),
context.read(),
);
_listenToInboxChanges();
context.read<ConnectivityCubit>().reload();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles();
});
}
void _listenToInboxChanges() {
_inboxCubit.refreshItemsInInboxCount();
_inboxTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
if (!mounted) {
timer.cancel();
} else {
_inboxCubit.refreshItemsInInboxCount();
}
});
}
@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();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_inboxTimer.cancel();
_inboxCubit.close();
super.dispose();
}
void _listenForReceivedFiles() async {
if (ShareIntentQueue.instance.hasUnhandledFiles) {
await _handleReceivedFile(ShareIntentQueue.instance.pop()!);
@@ -156,12 +187,6 @@ class _HomePageState extends State<HomePage> {
}
}
@override
void dispose() {
_inboxCubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final destinations = [
@@ -247,8 +272,18 @@ class _HomePageState extends State<HomePage> {
],
child: const LabelsPage(),
),
BlocProvider.value(
MultiBlocProvider(
providers: [
// We need to manually downcast the inboxcubit to the
// mixed-in DocumentPagingBlocMixin to use the
// DocumentPagingViewMixin in the inbox.
BlocProvider<DocumentPagingBlocMixin>.value(
value: _inboxCubit,
),
BlocProvider<InboxCubit>.value(
value: _inboxCubit,
),
],
child: const InboxPage(),
),
// const SettingsPage(),

View File

@@ -13,7 +13,7 @@
// import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
// import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
// import 'package:paperless_mobile/extensions/flutter_extensions.dart';
// import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
// import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
// import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
// import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';

View File

@@ -3,13 +3,9 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';

View File

@@ -1,14 +1,18 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
part 'inbox_cubit.g.dart';
part 'inbox_state.dart';
class InboxCubit extends HydratedCubit<InboxState>
with DocumentPagingBlocMixin {
final LabelRepository<Tag> _tagsRepository;
final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType> _documentTypeRepository;
@@ -82,13 +86,6 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
refreshItemsInInboxCount(false);
loadInbox();
Timer.periodic(const Duration(seconds: 5), (timer) {
if (isClosed) {
timer.cancel();
}
refreshItemsInInboxCount();
});
}
void refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {

View File

@@ -1,11 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
part 'inbox_state.g.dart';
part of 'inbox_cubit.dart';
@JsonSerializable(ignoreUnannotated: true)
class InboxState extends PagedDocumentsState {
class InboxState extends DocumentPagingState {
final Iterable<int> inboxTags;
final Map<int, Tag> availableTags;

View File

@@ -9,10 +9,10 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_list_
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/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.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';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -24,35 +24,15 @@ class InboxPage extends StatefulWidget {
State<InboxPage> createState() => _InboxPageState();
}
class _InboxPageState extends State<InboxPage> {
final ScrollController _scrollController = ScrollController();
class _InboxPageState extends State<InboxPage> with DocumentPagingViewMixin {
@override
final pagingScrollController = ScrollController();
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@override
void initState() {
super.initState();
context.read<InboxCubit>().loadInbox();
_scrollController.addListener(_listenForLoadNewData);
}
@override
void dispose() {
_scrollController.removeListener(_listenForLoadNewData);
super.dispose();
}
void _listenForLoadNewData() {
final currState = context.read<InboxCubit>().state;
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent * 0.75 &&
!currState.isLoading &&
!currState.isLastPageLoaded) {
try {
context.read<InboxCubit>().loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
@override
@@ -144,7 +124,7 @@ class _InboxPageState extends State<InboxPage> {
physics: state.documents.isEmpty
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
controller: _scrollController,
controller: pagingScrollController,
slivers: [
SearchAppBar(
hintText: S.of(context).documentSearchSearchDocuments,

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class InboxEmptyWidget extends StatelessWidget {

View File

@@ -5,7 +5,7 @@ import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/correspondent_bloc_provider.dart';
class CorrespondentWidget extends StatelessWidget {
final int? correspondentId;

View File

@@ -3,7 +3,8 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
part 'label_state.dart';
class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final LabelRepository<T> _repository;

View File

@@ -1,4 +1,4 @@
import 'package:paperless_api/paperless_api.dart';
part of 'label_cubit.dart';
class LabelState<T extends Label> {
LabelState.initial() : this(isLoaded: false, labels: {});

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class CorrespondentBlocProvider extends StatelessWidget {
final Widget child;

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class DocumentTypeBlocProvider extends StatelessWidget {
final Widget child;

View File

@@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class LabelsBlocProvider extends StatelessWidget {
final Widget child;

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class StoragePathBlocProvider extends StatelessWidget {
final Widget child;

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class TagBlocProvider extends StatelessWidget {
final Widget child;

View File

@@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/document_type_bloc_provider.dart';
class DocumentTypeWidget extends StatelessWidget {
final int? documentTypeId;

View File

@@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/storage_path_bloc_provider.dart';
class StoragePathWidget extends StatelessWidget {
final int? pathId;

View File

@@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatelessWidget {

View File

@@ -15,7 +15,7 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_corresponden
import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart';
import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/view/pages/linked_documents_page.dart';
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
class LabelItem<T extends Label> extends StatelessWidget {

View File

@@ -3,9 +3,8 @@ 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/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';

View File

@@ -2,8 +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/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class LabelText<T extends Label> extends StatelessWidget {
final int? id;

View File

@@ -1,37 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState>
with PagedDocumentsMixin {
@override
final PaperlessDocumentsApi api;
@override
final DocumentChangedNotifier notifier;
LinkedDocumentsCubit(
DocumentFilter filter,
this.api,
this.notifier,
) : super(const LinkedDocumentsState()) {
updateFilter(filter: filter);
notifier.subscribe(
this,
onUpdated: replace,
onDeleted: remove,
);
}
@override
Future<void> update(DocumentModel document) async {
final updated = await api.update(document);
if (!state.filter.matches(updated)) {
remove(document);
} else {
replace(document);
}
}
}

View File

@@ -0,0 +1,56 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'linked_documents_state.dart';
part 'linked_documents_cubit.g.dart';
class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@override
final DocumentChangedNotifier notifier;
LinkedDocumentsCubit(
DocumentFilter filter,
this.api,
this.notifier,
) : super(const LinkedDocumentsState()) {
updateFilter(filter: filter);
notifier.subscribe(
this,
onUpdated: replace,
onDeleted: remove,
);
}
@override
Future<void> update(DocumentModel document) async {
final updated = await api.update(document);
if (!state.filter.matches(updated)) {
remove(document);
} else {
replace(document);
}
}
void setViewType(ViewType type) {
emit(state.copyWith(viewType: type));
}
@override
LinkedDocumentsState? fromJson(Map<String, dynamic> json) {
return LinkedDocumentsState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(LinkedDocumentsState state) {
return state.toJson();
}
}

View File

@@ -1,8 +1,11 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
part of 'linked_documents_cubit.dart';
class LinkedDocumentsState extends PagedDocumentsState {
@JsonSerializable(ignoreUnannotated: true)
class LinkedDocumentsState extends DocumentPagingState {
@JsonKey()
final ViewType viewType;
const LinkedDocumentsState({
this.viewType = ViewType.list,
super.filter,
super.isLoading,
super.hasLoaded,
@@ -14,12 +17,14 @@ class LinkedDocumentsState extends PagedDocumentsState {
bool? isLoading,
bool? hasLoaded,
List<PagedSearchResult<DocumentModel>>? value,
ViewType? viewType,
}) {
return LinkedDocumentsState(
filter: filter ?? this.filter,
isLoading: isLoading ?? this.isLoading,
hasLoaded: hasLoaded ?? this.hasLoaded,
value: value ?? this.value,
viewType: viewType ?? this.viewType,
);
}
@@ -40,9 +45,12 @@ class LinkedDocumentsState extends PagedDocumentsState {
@override
List<Object?> get props => [
filter,
isLoading,
hasLoaded,
value,
viewType,
...super.props,
];
factory LinkedDocumentsState.fromJson(Map<String, dynamic> json) =>
_$LinkedDocumentsStateFromJson(json);
Map<String, dynamic> toJson() => _$LinkedDocumentsStateToJson(this);
}

View File

@@ -0,0 +1,72 @@
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/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class LinkedDocumentsPage extends StatefulWidget {
const LinkedDocumentsPage({super.key});
@override
State<LinkedDocumentsPage> createState() => _LinkedDocumentsPageState();
}
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
with DocumentPagingViewMixin {
@override
final pagingScrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).linkedDocumentsPageTitle),
actions: [
BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
builder: (context, state) {
return ViewTypeSelectionWidget(
viewType: state.viewType,
onChanged: context.read<LinkedDocumentsCubit>().setViewType,
);
},
),
],
),
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
builder: (context, state) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivity) {
return CustomScrollView(
controller: pagingScrollController,
slivers: [
SliverAdaptiveDocumentsView(
viewType: state.viewType,
documents: state.documents,
hasInternetConnection: connectivity.isConnected,
isLabelClickable: false,
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
onTap: (document) {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
),
);
},
),
],
);
},
);
},
),
);
}
}

View File

@@ -1,76 +0,0 @@
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/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class LinkedDocumentsPage extends StatefulWidget {
const LinkedDocumentsPage({super.key});
@override
State<LinkedDocumentsPage> createState() => _LinkedDocumentsPageState();
}
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_listenForLoadNewData);
}
void _listenForLoadNewData() async {
final currState = context.read<LinkedDocumentsCubit>().state;
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent * 0.75 &&
!currState.isLoading &&
!currState.isLastPageLoaded) {
try {
await context.read<LinkedDocumentsCubit>().loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).linkedDocumentsPageTitle),
),
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
builder: (context, state) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivity) {
return DefaultAdaptiveDocumentsView(
scrollController: _scrollController,
documents: state.documents,
hasInternetConnection: connectivity.isConnected,
isLabelClickable: false,
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
onTap: (document) {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
),
);
},
);
},
);
},
),
);
}
}

View File

@@ -1,12 +1,14 @@
import 'package:dio/dio.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
part 'authentication_state.dart';
part 'authentication_cubit.g.dart';
class AuthenticationCubit extends Cubit<AuthenticationState>
with HydratedMixin<AuthenticationState> {

View File

@@ -1,12 +1,9 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
part 'authentication_state.g.dart';
part of 'authentication_cubit.dart';
@JsonSerializable()
class AuthenticationState {
final bool wasLoginStored;
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
final bool? wasLocalAuthenticationSuccessful;
final AuthenticationInformation? authentication;

View File

@@ -3,17 +3,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/constants.dart';
import 'widgets/never_scrollable_scroll_behavior.dart';
import 'widgets/login_pages/server_login_page.dart';
import 'widgets/never_scrollable_scroll_behavior.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);

View File

@@ -63,25 +63,48 @@ class _ClientCertificateFormFieldState
),
child: Column(
children: [
ListTile(
leading: ElevatedButton(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ElevatedButton(
onPressed: () => _onSelectFile(field),
child: Text(S.of(context).genericActionSelectText),
child:
Text(S.of(context).genericActionSelectText),
),
title: _buildSelectedFileText(field),
trailing: AbsorbPointer(
absorbing: field.value == null,
child: _selectedFile != null
? IconButton(
_buildSelectedFileText(field).paddedOnly(left: 8),
],
),
if (_selectedFile != null)
IconButton(
icon: const Icon(Icons.close),
onPressed: () => setState(() {
_selectedFile = null;
field.didChange(null);
}),
)
: null,
),
),
],
).padded(8),
// ListTile(
// leading: ElevatedButton(
// onPressed: () => _onSelectFile(field),
// child: Text(S.of(context).genericActionSelectText),
// ),
// title: _buildSelectedFileText(field),
// trailing: AbsorbPointer(
// absorbing: field.value == null,
// child: _selectedFile != null
// ? IconButton(
// icon: const Icon(Icons.close),
// onPressed: () => setState(() {
// _selectedFile = null;
// field.didChange(null);
// }),
// )
// : null,
// ),
// ),
if (_selectedFile != null) ...[
ObscuredInputTextFormField(
key: const ValueKey('login-client-cert-passphrase'),
@@ -127,7 +150,9 @@ class _ClientCertificateFormFieldState
assert(_selectedFile == null);
return Text(
S.of(context).loginPageClientCertificateSettingSelectFileText,
style: TextStyle(color: Theme.of(context).hintColor),
style: Theme.of(context).textTheme.labelMedium?.apply(
color: Theme.of(context).hintColor,
),
);
} else {
assert(_selectedFile != null);

View File

@@ -1,17 +1,15 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'model/paged_documents_state.dart';
import 'paged_documents_state.dart';
///
/// Mixin which can be used on cubits that handle documents.
/// This implements all paging and filtering logic.
///
mixin PagedDocumentsMixin<State extends PagedDocumentsState>
mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
on BlocBase<State> {
PaperlessDocumentsApi get api;
DocumentChangedNotifier get notifier;

View File

@@ -5,13 +5,13 @@ import 'package:paperless_api/paperless_api.dart';
/// Base state for all blocs/cubits using a paged view of documents.
/// [T] is the return type of the API call.
///
abstract class PagedDocumentsState extends Equatable {
abstract class DocumentPagingState extends Equatable {
final bool hasLoaded;
final bool isLoading;
final List<PagedSearchResult<DocumentModel>> value;
final DocumentFilter filter;
const PagedDocumentsState({
const DocumentPagingState({
this.value = const [],
this.hasLoaded = false,
this.isLoading = false,

View File

@@ -0,0 +1,43 @@
import 'package:flutter/widgets.dart';
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> {
ScrollController get pagingScrollController;
@override
void initState() {
super.initState();
pagingScrollController.addListener(shouldLoadMoreDocumentsListener);
}
@override
void dispose() {
pagingScrollController.removeListener(shouldLoadMoreDocumentsListener);
super.dispose();
}
DocumentPagingBlocMixin get _bloc => context.read<DocumentPagingBlocMixin>();
void shouldLoadMoreDocumentsListener() async {
if (shouldLoadMoreDocuments) {
try {
await _bloc.loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
bool get shouldLoadMoreDocuments {
final currState = _bloc.state;
return pagingScrollController.position.maxScrollExtent != 0 &&
!currState.isLoading &&
!currState.isLastPageLoaded &&
pagingScrollController.offset >=
pagingScrollController.position.maxScrollExtent * 0.75;
}
}

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
part 'saved_view_state.dart';
class SavedViewCubit extends Cubit<SavedViewState> {
final SavedViewRepository _repository;

View File

@@ -1,30 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
part 'saved_view_details_state.dart';
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
with PagedDocumentsMixin {
@override
final PaperlessDocumentsApi api;
@override
final DocumentChangedNotifier notifier;
final SavedView savedView;
SavedViewDetailsCubit(
this.api,
this.notifier, {
required this.savedView,
}) : super(const SavedViewDetailsState()) {
notifier.subscribe(
this,
onDeleted: remove,
onUpdated: replace,
);
updateFilter(filter: savedView.toDocumentFilter());
}
}

View File

@@ -1,7 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
part of 'saved_view_cubit.dart';
class SavedViewState with EquatableMixin {
class SavedViewState extends Equatable {
final bool hasLoaded;
final Map<int, SavedView> value;

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
class AddSavedViewPage extends StatefulWidget {
final DocumentFilter currentFilter;

View File

@@ -1,12 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_page.dart';
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class SavedViewList extends StatelessWidget {
@@ -48,7 +45,7 @@ class SavedViewList extends StatelessWidget {
),
),
],
child: SavedViewPage(
child: SavedViewDetailsPage(
onDelete: savedViewCubit.remove,
),
),

View File

@@ -1,130 +0,0 @@
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/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class SavedViewPage extends StatefulWidget {
final Future<void> Function(SavedView savedView) onDelete;
const SavedViewPage({
super.key,
required this.onDelete,
});
@override
State<SavedViewPage> createState() => _SavedViewPageState();
}
class _SavedViewPageState extends State<SavedViewPage> {
final _scrollController = ScrollController();
ViewType _viewType = ViewType.list;
SavedView get _savedView => context.read<SavedViewDetailsCubit>().savedView;
@override
void initState() {
super.initState();
_scrollController.addListener(_listenForLoadNewData);
}
void _listenForLoadNewData() async {
final currState = context.read<SavedViewDetailsCubit>().state;
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent * 0.7 &&
!currState.isLoading &&
!currState.isLastPageLoaded) {
try {
await context.read<SavedViewDetailsCubit>().loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
builder: (context, state) {
return Text(_savedView.name);
},
),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) =>
ConfirmDeleteSavedViewDialog(view: _savedView),
) ??
false;
if (shouldDelete) {
await widget.onDelete(_savedView);
Navigator.pop(context);
}
},
),
IconButton(
icon: Icon(
_viewType == ViewType.list ? Icons.grid_view_rounded : Icons.list,
),
onPressed: () => setState(() => _viewType = _viewType.toggle()),
),
],
),
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
builder: (context, state) {
if (state.hasLoaded && state.documents.isEmpty) {
return DocumentsEmptyState(state: state);
}
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivity) {
return CustomScrollView(
controller: _scrollController,
slivers: [
SliverAdaptiveDocumentsView(
documents: state.documents,
hasInternetConnection: connectivity.isConnected,
isLabelClickable: false,
isLoading: state.isLoading,
hasLoaded: state.hasLoaded,
onTap: _onOpenDocumentDetails,
viewType: _viewType,
),
if (state.hasLoaded && state.isLoading)
const SliverToBoxAdapter(
child: Center(
child: CircularProgressIndicator(),
),
)
],
);
},
);
},
),
);
}
void _onOpenDocumentDetails(DocumentModel document) {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
),
);
}
}

View File

@@ -1,218 +0,0 @@
// import 'dart:math';
// 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/extensions/flutter_extensions.dart';
// import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
// import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
// import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
// import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
// import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
// import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
// import 'package:paperless_mobile/generated/l10n.dart';
// import 'package:paperless_mobile/helpers/message_helpers.dart';
// import 'package:paperless_mobile/constants.dart';
// import 'package:shimmer/shimmer.dart';
// class SavedViewSelectionWidget extends StatelessWidget {
// final DocumentFilter currentFilter;
// const SavedViewSelectionWidget({
// Key? key,
// required this.height,
// required this.enabled,
// required this.currentFilter,
// }) : super(key: key);
// final double height;
// final bool enabled;
// @override
// Widget build(BuildContext context) {
// return BlocBuilder<ConnectivityCubit, ConnectivityState>(
// builder: (context, connectivityState) {
// final hasInternetConnection = connectivityState.isConnected;
// return SizedBox(
// height: height,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisSize: MainAxisSize.min,
// children: [
// BlocBuilder<SavedViewCubit, SavedViewState>(
// builder: (context, state) {
// if (!state.hasLoaded) {
// return _buildLoadingWidget(context);
// }
// if (state.value.isEmpty) {
// return Text(S.of(context).savedViewsEmptyStateText);
// }
// return SizedBox(
// height: 38,
// child: ListView.separated(
// itemCount: state.value.length,
// scrollDirection: Axis.horizontal,
// itemBuilder: (context, index) {
// final view = state.value.values.elementAt(index);
// return GestureDetector(
// onLongPress: hasInternetConnection
// ? () => _onDelete(context, view)
// : null,
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
// builder: (context, docState) {
// final view = state.value.values.toList()[index];
// return FilterChip(
// label: Text(
// view.name,
// ),
// selected:
// view.id == docState.selectedSavedViewId,
// onSelected: enabled && hasInternetConnection
// ? (isSelected) =>
// _onSelected(isSelected, context, view)
// : null,
// );
// },
// ),
// );
// },
// separatorBuilder: (context, index) => const SizedBox(
// width: 4.0,
// ),
// ),
// );
// },
// ),
// BlocBuilder<SavedViewCubit, SavedViewState>(
// builder: (context, state) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// S.of(context).savedViewsLabel,
// style: Theme.of(context).textTheme.titleSmall,
// ),
// BlocBuilder<DocumentsCubit, DocumentsState>(
// buildWhen: (previous, current) =>
// previous.filter != current.filter,
// builder: (context, docState) {
// return TextButton.icon(
// icon: const Icon(Icons.add),
// onPressed: (enabled &&
// state.hasLoaded &&
// hasInternetConnection)
// ? () =>
// _onCreatePressed(context, docState.filter)
// : null,
// label: Text(S.of(context).savedViewCreateNewLabel),
// );
// },
// ),
// ],
// );
// },
// ),
// ],
// ).padded(),
// );
// },
// );
// }
// Widget _buildLoadingWidget(BuildContext context) {
// return SizedBox(
// height: 38,
// width: MediaQuery.of(context).size.width,
// child: Shimmer.fromColors(
// baseColor: Theme.of(context).brightness == Brightness.light
// ? Colors.grey[300]!
// : Colors.grey[900]!,
// highlightColor: Theme.of(context).brightness == Brightness.light
// ? Colors.grey[100]!
// : Colors.grey[600]!,
// child: ListView(
// scrollDirection: Axis.horizontal,
// physics: const NeverScrollableScrollPhysics(),
// children: [
// FilterChip(
// label: const SizedBox(width: 32),
// onSelected: (_) {},
// ),
// const SizedBox(width: 4.0),
// FilterChip(
// label: const SizedBox(width: 64),
// onSelected: (_) {},
// ),
// const SizedBox(width: 4.0),
// FilterChip(
// label: const SizedBox(width: 100),
// onSelected: (_) {},
// ),
// const SizedBox(width: 4.0),
// FilterChip(
// label: const SizedBox(width: 32),
// onSelected: (_) {},
// ),
// const SizedBox(width: 4.0),
// FilterChip(
// label: const SizedBox(width: 48),
// onSelected: (_) {},
// ),
// ],
// ),
// ),
// );
// }
// void _onCreatePressed(BuildContext context, DocumentFilter filter) async {
// final newView = await Navigator.of(context).push<SavedView?>(
// MaterialPageRoute(
// builder: (context) => AddSavedViewPage(
// currentFilter: filter,
// ),
// ),
// );
// if (newView != null) {
// try {
// await context.read<SavedViewCubit>().add(newView);
// } on PaperlessServerException catch (error, stackTrace) {
// showErrorMessage(context, error, stackTrace);
// }
// }
// }
// void _onSelected(
// bool selectionIntent,
// BuildContext context,
// SavedView view,
// ) async {
// if (selectionIntent) {
// context.read<DocumentsCubit>().selectView(view.id!);
// } else {
// context.read<DocumentsCubit>().unselectView();
// context.read<DocumentsCubit>().resetFilter();
// }
// }
// void _onDelete(BuildContext context, SavedView view) async {
// {
// final delete = await showDialog<bool>(
// context: context,
// builder: (context) => ConfirmDeleteSavedViewDialog(view: view),
// ) ??
// false;
// if (delete) {
// try {
// context.read<SavedViewCubit>().remove(view);
// if (context.read<DocumentsCubit>().state.selectedSavedViewId ==
// view.id) {
// await context.read<DocumentsCubit>().resetFilter();
// }
// } on PaperlessServerException catch (error, stackTrace) {
// showErrorMessage(context, error, stackTrace);
// }
// }
// }
// }
// }

View File

@@ -0,0 +1,47 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'saved_view_details_cubit.g.dart';
part 'saved_view_details_state.dart';
class SavedViewDetailsCubit extends HydratedCubit<SavedViewDetailsState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@override
final DocumentChangedNotifier notifier;
final SavedView savedView;
SavedViewDetailsCubit(
this.api,
this.notifier, {
required this.savedView,
}) : super(const SavedViewDetailsState()) {
notifier.subscribe(
this,
onDeleted: remove,
onUpdated: replace,
);
updateFilter(filter: savedView.toDocumentFilter());
}
void setViewType(ViewType viewType) {
emit(state.copyWith(viewType: viewType));
}
@override
SavedViewDetailsState? fromJson(Map<String, dynamic> json) {
return SavedViewDetailsState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(SavedViewDetailsState state) {
return state.toJson();
}
}

View File

@@ -1,7 +1,12 @@
part of 'saved_view_details_cubit.dart';
class SavedViewDetailsState extends PagedDocumentsState {
@JsonSerializable(ignoreUnannotated: true)
class SavedViewDetailsState extends DocumentPagingState {
@JsonKey()
final ViewType viewType;
const SavedViewDetailsState({
this.viewType = ViewType.list,
super.filter,
super.hasLoaded,
super.isLoading,
@@ -10,10 +15,8 @@ class SavedViewDetailsState extends PagedDocumentsState {
@override
List<Object?> get props => [
filter,
hasLoaded,
isLoading,
value,
viewType,
...super.props,
];
@override
@@ -36,12 +39,19 @@ class SavedViewDetailsState extends PagedDocumentsState {
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
ViewType? viewType,
}) {
return SavedViewDetailsState(
hasLoaded: hasLoaded ?? this.hasLoaded,
isLoading: isLoading ?? this.isLoading,
value: value ?? this.value,
filter: filter ?? this.filter,
viewType: viewType ?? this.viewType,
);
}
factory SavedViewDetailsState.fromJson(Map<String, dynamic> json) =>
_$SavedViewDetailsStateFromJson(json);
Map<String, dynamic> toJson() => _$SavedViewDetailsStateToJson(this);
}

View File

@@ -0,0 +1,104 @@
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/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
import 'package:paperless_mobile/routes/document_details_route.dart';
class SavedViewDetailsPage extends StatefulWidget {
final Future<void> Function(SavedView savedView) onDelete;
const SavedViewDetailsPage({
super.key,
required this.onDelete,
});
@override
State<SavedViewDetailsPage> createState() => _SavedViewDetailsPageState();
}
class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
with DocumentPagingViewMixin {
@override
final pagingScrollController = ScrollController();
@override
Widget build(BuildContext context) {
final cubit = context.read<SavedViewDetailsCubit>();
return Scaffold(
appBar: AppBar(
title: Text(cubit.savedView.name),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => ConfirmDeleteSavedViewDialog(
view: cubit.savedView,
),
) ??
false;
if (shouldDelete) {
await widget.onDelete(cubit.savedView);
Navigator.pop(context);
}
},
),
BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
builder: (context, state) {
return ViewTypeSelectionWidget(
viewType: state.viewType,
onChanged: cubit.setViewType,
);
},
)
],
),
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
builder: (context, state) {
if (state.hasLoaded && state.documents.isEmpty) {
return DocumentsEmptyState(state: state);
}
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,
onTap: (document) {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: document,
isLabelClickable: false,
),
);
},
viewType: state.viewType,
),
if (state.hasLoaded && state.isLoading)
const SliverToBoxAdapter(
child: Center(
child: CircularProgressIndicator(),
),
)
],
);
},
);
},
),
);
}
}

View File

@@ -1,9 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/generated/l10n.dart';
part 'application_settings_cubit.g.dart';
part 'application_settings_state.dart';
class ApplicationSettingsCubit extends HydratedCubit<ApplicationSettingsState> {
final LocalAuthenticationService _localAuthenticationService;
@@ -33,11 +38,6 @@ class ApplicationSettingsCubit extends HydratedCubit<ApplicationSettingsState> {
_updateSettings(updatedSettings);
}
void setViewType(ViewType viewType) {
final updatedSettings = state.copyWith(preferredViewType: viewType);
_updateSettings(updatedSettings);
}
void setColorSchemeOption(ColorSchemeOption schemeOption) {
final updatedSettings =
state.copyWith(preferredColorSchemeOption: schemeOption);

Some files were not shown because too many files have changed in this diff Show More