mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 22:07:57 -06:00
Merge branch 'main' into feature/detailed-view-type
This commit is contained in:
17
README.md
17
README.md
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: DocumentDownloadButton(
|
||||
document: state.document,
|
||||
enabled: isConnected,
|
||||
metaData: _metaData,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user