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.
### 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 dependencies and generate files
@@ -83,11 +81,18 @@ To get a local copy up and running follow these simple steps.
```sh
git clone https://github.com/astubenbord/paperless-mobile.git
```
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:
In this project, flutter is pinned at a specific version as a git submodule to ensure all contributors work with the same environment and build with the same flutter version. You can also use your local flutter installation, just make sure that the app also compiles with the same flutter version as pinned in the `flutter` submodule when opening a pull request.
To download the pinned flutter SDK from the submodule and plan to install the dependencies manually in the next step, simply run
```sh
git submodule update --init
```
You can now run the `scripts/install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate files for both the app and 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:
2. Install the dependencies for `paperless_api`
```sh
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):
```sh
flutter build apk

View File

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

View File

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

View File

@@ -66,6 +66,8 @@
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<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>
</plist>

View File

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

View File

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

View File

@@ -4,17 +4,23 @@ import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/file_service.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/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:permission_handler/permission_handler.dart';
class DocumentDownloadButton extends StatefulWidget {
final DocumentModel? document;
final bool enabled;
final Future<DocumentMetaData> metaData;
const DocumentDownloadButton({
super.key,
required this.document,
this.enabled = true,
required this.metaData,
});
@override
@@ -34,28 +40,58 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
width: 16,
)
: const Icon(Icons.download),
onPressed: Platform.isAndroid && widget.document != null && widget.enabled
onPressed: widget.document != null && widget.enabled
? () => _onDownload(widget.document!)
: null,
).paddedOnly(right: 4);
}
Future<void> _onDownload(DocumentModel document) async {
if (!Platform.isAndroid) {
showSnackBar(
context, "This feature is currently only supported on Android!");
return;
}
setState(() => _isDownloadPending = true);
final service = context.read<PaperlessDocumentsApi>();
final api = context.read<PaperlessDocumentsApi>();
final meta = await widget.metaData;
try {
final bytes = await service.download(document);
final meta = await service.getMetaData(document);
final downloadOriginal = await showDialog<bool>(
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;
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);
createdFile.createSync(recursive: true);
createdFile.writeAsBytesSync(bytes);
debugPrint("Downloaded file to $filePath");
showSnackBar(context, S.of(context).documentDownloadSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);

View File

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

View File

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