mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 20:07:55 -06:00
Merge branch 'main' into feature/detailed-view-type
This commit is contained in:
15
README.md
15
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.
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user