Merge branch 'main' into feature/detailed-view-type

This commit is contained in:
Anton Stubenbord
2023-02-14 23:43:58 +01:00
9 changed files with 103 additions and 33 deletions

View File

@@ -74,8 +74,6 @@ With this app you can conveniently add, manage or simply find documents stored i
To get a local copy up and running follow these simple steps. To get a local copy up and running follow these simple steps.
### Prerequisites ### Prerequisites
* Install the Flutter SDK (https://docs.flutter.dev/get-started/install)
* Install an IDE of your choice (e.g. VSCode with the Dart/Flutter extensions) * Install an IDE of your choice (e.g. VSCode with the Dart/Flutter extensions)
### Install dependencies and generate files ### Install dependencies and generate files
@@ -83,11 +81,18 @@ To get a local copy up and running follow these simple steps.
```sh ```sh
git clone https://github.com/astubenbord/paperless-mobile.git git clone https://github.com/astubenbord/paperless-mobile.git
``` ```
In this project, flutter is pinned at a specific version as a git submodule to ensure all contributors work with the same environment and build with the same flutter version. You can also use your local flutter installation, just make sure that the app also compiles with the same flutter version as pinned in the `flutter` submodule when opening a pull request.
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: To download the pinned flutter SDK from the submodule and plan to install the dependencies manually in the next step, simply run
```sh
git submodule update --init
```
You can now run the `scripts/install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate files for both the app and subpackages. Note that the `install_dependencies.sh` script will pull the flutter submodule and use the SDK to execute the flutter commands.
If you don't want to use submodules, you can also run the following commands using your local flutter installation:
#### Inside the `packages/paperless_api/` folder: #### Inside the `packages/paperless_api/` folder:
2. Install the dependencies for `paperless_api` 2. Install the dependencies for `paperless_api`
```sh ```sh
flutter pub get flutter pub get
@@ -129,6 +134,8 @@ buildTypes {
} }
} }
``` ```
or use your own signing configuration as described in https://docs.flutter.dev/deployment/android#signing-the-app and leave the `build.gradle` as is.
2. Build the app with release profile (here for android): 2. Build the app with release profile (here for android):
```sh ```sh
flutter build apk flutter build apk

View File

@@ -1,11 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.paperless_mobile"> package="com.example.paperless_mobile">
<application
android:requestLegacyExternalStorage="true"/>
<!-- Flutter needs it to communicate with the running application <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> android:maxSdkVersion="29"/>
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> -->
</manifest> </manifest>

View File

@@ -2,7 +2,8 @@
package="com.example.paperless_mobile"> package="com.example.paperless_mobile">
<application android:label="Paperless Mobile" <application android:label="Paperless Mobile"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon"
android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -301,9 +302,9 @@
</application> </application>
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" /> -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View File

@@ -66,6 +66,8 @@
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Allow this app access to your camera to scan documents.</string> <string>Allow this app access to your camera to scan documents.</string>
<key>UISupportsDocumentBrowser</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -50,7 +50,10 @@ class FileService {
))! ))!
.first; .first;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
return getApplicationDocumentsDirectory(); final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/documents');
dir.createSync();
return dir;
} else { } else {
throw UnsupportedError("Platform not supported."); throw UnsupportedError("Platform not supported.");
} }
@@ -58,11 +61,19 @@ class FileService {
static Future<Directory> get downloadsDirectory async { static Future<Directory> get downloadsDirectory async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return (await getExternalStorageDirectories( Directory directory = Directory('/storage/emulated/0/Download');
type: StorageDirectory.downloads))! if (!directory.existsSync()) {
.first; final downloadsDir = await getExternalStorageDirectories(
type: StorageDirectory.downloads,
);
directory = downloadsDir!.first;
}
return directory;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
return getApplicationDocumentsDirectory(); final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/downloads');
dir.createSync();
return dir;
} else { } else {
throw UnsupportedError("Platform not supported."); throw UnsupportedError("Platform not supported.");
} }
@@ -70,10 +81,15 @@ class FileService {
static Future<Directory?> get scanDirectory async { static Future<Directory?> get scanDirectory async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return (await getExternalStorageDirectories(type: StorageDirectory.dcim))! final scanDir = await getExternalStorageDirectories(
.first; type: StorageDirectory.dcim,
);
return scanDir!.first;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
return getApplicationDocumentsDirectory(); final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/scans');
dir.createSync();
return dir;
} else { } else {
throw UnsupportedError("Platform not supported."); throw UnsupportedError("Platform not supported.");
} }

View File

@@ -244,6 +244,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: DocumentDownloadButton( child: DocumentDownloadButton(
document: state.document, document: state.document,
enabled: isConnected, enabled: isConnected,
metaData: _metaData,
), ),
), ),
IconButton( IconButton(

View File

@@ -4,17 +4,23 @@ import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/helpers/permission_helpers.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:permission_handler/permission_handler.dart';
class DocumentDownloadButton extends StatefulWidget { class DocumentDownloadButton extends StatefulWidget {
final DocumentModel? document; final DocumentModel? document;
final bool enabled; final bool enabled;
final Future<DocumentMetaData> metaData;
const DocumentDownloadButton({ const DocumentDownloadButton({
super.key, super.key,
required this.document, required this.document,
this.enabled = true, this.enabled = true,
required this.metaData,
}); });
@override @override
@@ -34,28 +40,58 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
width: 16, width: 16,
) )
: const Icon(Icons.download), : const Icon(Icons.download),
onPressed: Platform.isAndroid && widget.document != null && widget.enabled onPressed: widget.document != null && widget.enabled
? () => _onDownload(widget.document!) ? () => _onDownload(widget.document!)
: null, : null,
).paddedOnly(right: 4); ).paddedOnly(right: 4);
} }
Future<void> _onDownload(DocumentModel document) async { Future<void> _onDownload(DocumentModel document) async {
if (!Platform.isAndroid) { final api = context.read<PaperlessDocumentsApi>();
showSnackBar( final meta = await widget.metaData;
context, "This feature is currently only supported on Android!");
return;
}
setState(() => _isDownloadPending = true);
final service = context.read<PaperlessDocumentsApi>();
try { try {
final bytes = await service.download(document); final downloadOriginal = await showDialog<bool>(
final meta = await service.getMetaData(document); context: context,
builder: (context) => RadioSettingsDialog(
titleText: "Choose filetype", //TODO: INTL
options: [
RadioOption(
value: true,
label:
"Original (${meta.originalMimeType.split("/").last})", //TODO: INTL
),
RadioOption(
value: false,
label: "Archived (pdf)", //TODO: INTL
),
],
initialValue: false,
),
);
if (downloadOriginal == null) {
// Download was cancelled
return;
}
if (Platform.isAndroid && androidInfo!.version.sdkInt! < 30) {
final isGranted = await askForPermission(Permission.storage);
if (!isGranted) {
return;
}
}
setState(() => _isDownloadPending = true);
final bytes = await api.download(
document,
original: downloadOriginal,
);
final Directory dir = await FileService.downloadsDirectory; final Directory dir = await FileService.downloadsDirectory;
String filePath = "${dir.path}/${meta.mediaFilename}"; final fileExtension =
downloadOriginal ? meta.mediaFilename.split(".").last : 'pdf';
String filePath = "${dir.path}/${meta.mediaFilename}".split(".").first;
filePath += ".$fileExtension";
final createdFile = File(filePath); final createdFile = File(filePath);
createdFile.createSync(recursive: true); createdFile.createSync(recursive: true);
createdFile.writeAsBytesSync(bytes); createdFile.writeAsBytesSync(bytes);
debugPrint("Downloaded file to $filePath");
showSnackBar(context, S.of(context).documentDownloadSuccessMessage); showSnackBar(context, S.of(context).documentDownloadSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);

View File

@@ -23,7 +23,7 @@ abstract class PaperlessDocumentsApi {
Future<Iterable<int>> bulkAction(BulkAction action); Future<Iterable<int>> bulkAction(BulkAction action);
Future<Uint8List> getPreview(int docId); Future<Uint8List> getPreview(int docId);
String getThumbnailUrl(int docId); String getThumbnailUrl(int docId);
Future<Uint8List> download(DocumentModel document); Future<Uint8List> download(DocumentModel document, {bool original});
Future<FieldSuggestions> findSuggestions(DocumentModel document); Future<FieldSuggestions> findSuggestions(DocumentModel document);
Future<List<String>> autocomplete(String query, [int limit = 10]); Future<List<String>> autocomplete(String query, [int limit = 10]);

View File

@@ -192,10 +192,14 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
@override @override
Future<Uint8List> download(DocumentModel document) async { Future<Uint8List> download(
DocumentModel document, {
bool original = false,
}) async {
try { try {
final response = await client.get( final response = await client.get(
"/api/documents/${document.id}/download/", "/api/documents/${document.id}/download/",
queryParameters: original ? {'original': true} : {},
options: Options(responseType: ResponseType.bytes), options: Options(responseType: ResponseType.bytes),
); );
return response.data; return response.data;