From 88a7ec3ea13caa74e3dc1da4a1109c92327000bb Mon Sep 17 00:00:00 2001 From: Alex Yackers <7115964+yackers@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:25:48 +0200 Subject: [PATCH] Features, Fixes, Chore. - Feature: instantiation `DocumentThumbnail` `fromUri()` static method. - Feature: instantiation `DocumentFile` `fromUri()` static method. - Fixes: for `share()` `open()` - title is optional parameter - Chore: updated README.md, CHANGELOG.md, documentation, example, dependencies, gradle, kotlin, java. Signed-off-by: Alex Yackers <7115964+yackers@users.noreply.github.com> --- CHANGELOG.md | 66 +++++++++++++++++++ README.md | 46 ++++++++++--- android/build.gradle | 14 ++-- example/android/app/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 4 +- .../documentfile/actions_document_file.dart | 39 ++++++++--- .../documentfile/activity_document_file.dart | 4 +- .../documentfile/init_document_file.dart | 10 ++- lib/src/data/document_file.dart | 11 ++++ lib/src/data/document_thumbnail.dart | 31 ++++++++- lib/src/extensions/document_file_ext.dart | 14 ++-- 12 files changed, 207 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac11106..027bcc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ +## 1.2.0 + +* New Feature: Added method `all()` for `DocMan.dir` helper. + It's a simple way to get all app directories in one call as map. + + ```dart + /// Get all app directories as map with keys as directory names and values as paths. + final Map dirs = await DocMan.dir.all(); + + ///Result example: + { + "cache": "/data/user/0/com.example.app/cache", + "files": "/data/user/0/com.example.app/files", + "data": "/data/user/0/com.example.app/app_flutter", + // External directories, can be empty strings if not available. + "cacheExt": "/storage/emulated/0/Android/data/com.example.app/cache", + "filesExt": "/storage/emulated/0/Android/data/com.example.app/files", + } + ``` +* Feature: Added ability to instantiate `DocumentThumbnail` from `Content Uri` or `File.path`. + Same as `DocumentFile.thumbnail()` method, but now you can get `DocumentThumbnail` directly. + + ```dart + /// Create thumbnail from content uri or file path. + final DocumentThumbnail thumbnail = await DocumentThumbnail.fromUri( + contentUriOrFilePath, + width: 100, + height: 100, + png: true, + ); + ``` +* Feature: Added syntax sugar for `DocumentFile` instantiation from `Content Uri` or `File.path`. + + ```dart + /// New syntax sugar for DocumentFile instantiation from content uri or file path. + final DocumentFile? doc = await DocumentFile.fromUri(contentUriOrFilePath); + + /// Old way. + final DocumentFile? doc = await DocumentFile(contentUriOrFilePath).get(); + ``` +* Fix: problem with parallel calls to `DocumentFile` `action` methods, when working with different + documents. + Now it's fixed. `DocManQueueManager` was primarily used for all `activity` methods, and it caused the problem. + For example, now it's possible to create list or grid of documents thumbnails without any problems. + If you were getting errors in log like `Error loading thumbnail: AlreadyRunning Method: documentfileaction`, it should + be fixed now. + +* Fix: error in syntax in methods `DocumentFile.share()` & `DocumentFile.open()`. + String `title` parameter was not optional, but it should be. Now it's fixed. + + Please check your code and change `title` parameter to optional if you are using these methods. + + **From:** + ```dart + final bool share = await doc.share('Share this document:'); + final bool open = await doc.open('Open with:'); + ``` + + **To:** + ```dart + final bool share = await doc.share(title: 'Share this document:'); + final bool open = await doc.open(title: 'Open with:'); + ``` + +* Chore: Updated dependencies, updated example, updated README, some code cleanup & small fixes. + ## 1.1.0 * New Feature: Implemented simple custom `DocumentsProvider` diff --git a/README.md b/README.md index 204ef45..a009c67 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![License: MIT](https://img.shields.io/github/license/devdfcom/docman?style=flat&color=mediumseagreen)](https://opensource.org/licenses/MIT) [![Request a feature](https://img.shields.io/badge/Request-Feature-teal?style=flat)](https://github.com/devdfcom/docman/discussions/new?category=ideas) [![Ask a question](https://img.shields.io/badge/Ask-Question-royalblue?style=flat)](https://github.com/devdfcom/docman/discussions/new/choose) -[![Report a bug](https://img.shields.io/badge/Report-Bug-indianred?style=flat)](https://github.com/devdfcom/docman/issues/new?labels=bug&projects=&template=bug_report.yml&title=%3Ctitle%3E) +[![Report a bug](https://img.shields.io/badge/Report-Bug-indianred?style=flat)](https://github.com/devdfcom/docman/issues/new?labels=bug&projects=&template=bug_report.yml) A Flutter plugin that simplifies file & directory operations on Android devices. Leveraging the Storage Access Framework (SAF) API, @@ -277,7 +277,7 @@ Only `cacheExt` & `filesExt` can be empty strings if external storage is not ava ///Get all directories at once via helper method Future getAllDirs() async { final Map dirs = await DocMan.dir.all(); - + print(dirs); } @@ -504,6 +504,9 @@ There are two ways to instantiate a `DocumentFile`: ```dart Future backupDir() => DocumentFile(uri: 'content://com.android.externalstorage.documents/tree/primary%3ADocMan').get(); + ///Or you can use static method fromUri + Future backupDir() => + DocumentFile.fromUri('content://com.android.externalstorage.documents/tree/primary%3ADocMan'); ``` > [!CAUTION] @@ -516,9 +519,13 @@ There are two ways to instantiate a `DocumentFile`: ```dart Future file() => DocumentFile(uri: 'path/to/file.jpg').get(); + ///Or you can use static method fromUri + Future file() => DocumentFile.fromUri('path/to/file.jpg'); /// If directory doesn't exist, it will create all directories in the path. Future dir() => DocumentFile(uri: 'path/to/some/directory/notCreatedYet').get(); + ///Or you can use static method fromUri + Future dir() => DocumentFile.fromUri('path/to/some/directory/notCreatedYet'); ``` #### **DocumentFile Activity methods** @@ -532,15 +539,19 @@ Like `open`, `share`, `saveTo` methods. All methods are called through Activity the system will show a dialog to choose the app to open with. Action can be performed only on file & file must exist. - ```dart - Future openFile(DocumentFile file) => file.open('Open with:'); - ``` + * `title` parameter is optional, and not working on all devices, depends on the system. + + ```dart + Future openFile(DocumentFile file) => file.open(title: 'Open with:'); //or file.open(); + ``` - `share` `📄` Share the file with other apps. - ```dart - Future shareFile(DocumentFile file) => file.share('Share with:'); - ``` + * `title` parameter is optional, and not working on all devices, depends on the system. + + ```dart + Future shareFile(DocumentFile file) => file.share(title: 'Share with:'); //or file.share(); + ``` - `saveTo` `📄` Save the file to the selected directory. @@ -736,6 +747,8 @@ and can be performed in the background (with isolates or WorkManager). file.moveTo('/data/user/0/devdf.plugins.docman_example/cache/TempDir', name: 'moved_file.txt'); ``` + + - `thumbnail` `📄` Get the thumbnail of the file. Can be used only on file & file must exist & has flag `canThumbnail` set to `true`. @@ -768,9 +781,24 @@ and can be performed in the background (with isolates or WorkManager). #### 🧩 **DocumentThumbnail class** -`DocumentThumbnail` is a data class that holds information about the thumbnail image. +`DocumentThumbnail` is a class that holds information about the thumbnail image. It stores the `width`, `height` of the image, and the `bytes` (Uint8List) of the image. +`📄` You can instantiate the `DocumentThumbnail` via static method `fromUri()` or from `DocumentFile` instance via +[`DocumentFile.thumbnail()`](#documentfile-action-thumbnail) method. + +You must specify the width & height of the thumbnail. Optionally you can specify the quality of the image +and set `png` or `webp` to `true` to get the compressed image in that format, otherwise it will be `jpeg`. +Returns [DocumentThumbnail](#-documentthumbnail-class) instance of the thumbnail image or `null` if the thumbnail is +not available. Commonly used for images, videos, pdfs. + +```dart +/// It can be instantiated from `Content Uri` or `File.path` via static method. +Future getThumbnail(String contentUriOrFilePath) async { + return await DocumentThumbnail.fromUri(contentUriOrFilePath, width: 192, height: 192, png: true, quality: 100); +} +``` + #### **Unsupported methods** Information about currently (temporarily) unsupported methods in the plugin. diff --git a/android/build.gradle b/android/build.gradle index b35e006..02c86b9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group = "devdf.plugins.docman" version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = "1.8.22" + ext.kotlin_version = "2.0.20" repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.5.2") + classpath("com.android.tools.build:gradle:8.8.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } @@ -29,15 +29,15 @@ android { namespace = "devdf.plugins.docman" } - compileSdk = 34 + compileSdk = 35 compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 } sourceSets { @@ -69,5 +69,5 @@ android { dependencies { implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'androidx.activity:activity-ktx:1.9.3' + implementation 'androidx.activity:activity-ktx:1.10.0' } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index aaea786..b6f8450 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -11,12 +11,12 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 } defaultConfig { diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3c85cfe..ac3b479 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 69b81b8..75938d2 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.4" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.8.0" apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false } include ":app" diff --git a/example/lib/src/ui/pages/documentfile/actions_document_file.dart b/example/lib/src/ui/pages/documentfile/actions_document_file.dart index 14559e1..c85842d 100644 --- a/example/lib/src/ui/pages/documentfile/actions_document_file.dart +++ b/example/lib/src/ui/pages/documentfile/actions_document_file.dart @@ -120,7 +120,10 @@ class _ActionsDocumentFileState extends State { ? exception.title : 'Permissions for: ${_document?.name}', subTitle: exception?.subTitle, - result: exception?.result ?? perms.toString(), + result: exception?.result ?? + (perms == null + ? 'DocumentFile has no persisted permissions' + : perms.toString()), isResultOk: exception == null && perms != null, ), ]); @@ -431,24 +434,44 @@ class _ActionsDocumentFileState extends State { ) ]); } else { + //2.1. Set thumbnail method + //There are currently 2 ways to get thumbnail + // First to get thumbnail via `document.thumbnail()` method or by instantiating `DocumentThumbnail` class + // Both methods are same, depending on what you prefer + // Here commented out example of `document.thumbnail()` method, old one, just to show the difference + + /// final thumbnailMethod = _document!.thumbnail( + /// width: _thumbWidth, + /// height: _thumbHeight, + /// quality: _thumbQuality, + /// png: png, + /// webp: webp, + /// ); + + //2.2. Get thumbnail via `DocumentThumbnail.fromUri()` method + final thumbnailMethod = DocumentThumbnail.fromUri( + _document!.uri, + width: _thumbWidth, + height: _thumbHeight, + quality: _thumbQuality, + png: png, + webp: webp, + ); + //3. Set thumbnail widget to result widget.onResultWidgets([ MethodApiWidget( MethodApiEntry( + //name: 'DocumentFile.thumbnail(width: $_thumbWidth, height: $_thumbHeight, quality: $_thumbQuality, png: $png, webp: $webp)', name: - 'DocumentFile.thumbnail(width: $_thumbWidth, height: $_thumbHeight, quality: $_thumbQuality, png: $png, webp: $webp)', + 'DocumentThumbnail.fromUri(${_document!.uri}, width: $_thumbWidth, height: $_thumbHeight, quality: $_thumbQuality, png: $png, webp: $webp)', subTitle: 'Future intentionally delayed for 2 seconds'), endDivider: false, ), ThumbResultWidget( maxWidth: _thumbWidth, maxHeight: _thumbHeight, - getThumb: _document!.thumbnail( - width: _thumbWidth, - height: _thumbHeight, - quality: _thumbQuality, - png: png, - webp: webp), + getThumb: thumbnailMethod, ) ]); } diff --git a/example/lib/src/ui/pages/documentfile/activity_document_file.dart b/example/lib/src/ui/pages/documentfile/activity_document_file.dart index e6c438d..a861ae8 100644 --- a/example/lib/src/ui/pages/documentfile/activity_document_file.dart +++ b/example/lib/src/ui/pages/documentfile/activity_document_file.dart @@ -46,7 +46,7 @@ class _ActivityDocumentFileState extends State { MethodApiEntry? exception; try { - isOpened = await widget.document!.open('Open Document with'); + isOpened = await widget.document!.open(title: 'Open Document with'); } catch (e) { exception = _exceptionEntry(e); } @@ -71,7 +71,7 @@ class _ActivityDocumentFileState extends State { MethodApiEntry? exception; try { - isShared = await widget.document!.share('Share Document with'); + isShared = await widget.document!.share(title: 'Share Document with'); } catch (e) { exception = _exceptionEntry(e); } diff --git a/example/lib/src/ui/pages/documentfile/init_document_file.dart b/example/lib/src/ui/pages/documentfile/init_document_file.dart index 3e2ebd5..334cca7 100644 --- a/example/lib/src/ui/pages/documentfile/init_document_file.dart +++ b/example/lib/src/ui/pages/documentfile/init_document_file.dart @@ -172,7 +172,12 @@ class _InitDocumentFileState extends State { final filePath = await _getFilePath(); //2. Create DocumentFile from file path try { - doc = await DocumentFile(uri: filePath).get(); + //2.1 In DocMan v1.2.0 was added static method `fromUri` for `DocumentFile` + //So from now on it's possible to instantiate DocumentFile via `DocumentFile.fromUri(uri)` + doc = await DocumentFile.fromUri(filePath); + + ///Before v1.2.0 it was possible to instantiate DocumentFile only via `DocumentFile(uri: uri).get()` + ///doc = await DocumentFile(uri: filePath).get(); } catch (e) { exception = _exceptionEntry(e); } @@ -183,7 +188,8 @@ class _InitDocumentFileState extends State { //4. Update the result widget.onResult([ MethodApiEntry( - name: 'DocumentFile(uri: $filePath).get()', + //name: DocumentFile(uri: $filePath).get() + name: 'DocumentFile.fromUri($filePath)', title: exception != null ? exception.title : 'DocumentFile: ${doc?.name}', diff --git a/lib/src/data/document_file.dart b/lib/src/data/document_file.dart index ed47284..eb9f5cf 100644 --- a/lib/src/data/document_file.dart +++ b/lib/src/data/document_file.dart @@ -1,3 +1,4 @@ +import 'package:docman/docman.dart'; import 'package:flutter/material.dart'; /// A class representing a `DocumentFile` on dart side. @@ -93,6 +94,16 @@ class DocumentFile { canThumbnail: map['canThumbnail'] as bool, ); + /// Instantiates a [DocumentFile] from a Content URI or a File path. + /// + /// Same as `DocumentFile(uri: uri).get()`, just syntactic sugar. + /// + /// **Note**: Or you can use old method `DocumentFile(uri: uri).get()` to get the `DocumentFile` instance. + /// + /// Returns a [DocumentFile] instance or `null` if the document is not available. + static Future fromUri(String uri) => + DocumentFile(uri: uri).get(); + /// Checks if the document is a directory. bool get isDirectory => type == 'directory'; diff --git a/lib/src/data/document_thumbnail.dart b/lib/src/data/document_thumbnail.dart index aae2ebf..26a0333 100644 --- a/lib/src/data/document_thumbnail.dart +++ b/lib/src/data/document_thumbnail.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:docman/docman.dart'; import 'package:flutter/material.dart'; /// A class that represents a document thumbnail. @@ -30,7 +31,35 @@ class DocumentThumbnail { height: map['height'] as int, ); - //TODO get thumbnail directly from filePath or Uri + /// Instantiates a [DocumentThumbnail] from a Content URI or a File path. + /// + /// [uri] - can be `Content Uri` saved from previous request with persisted permission, + /// or it can be app local `File.path` + /// + /// - [width] Width of the thumbnail, default is 256. + /// - [height] Height of the thumbnail, default is 256. + /// + /// [width] & [height] must be greater than 0. + /// + /// - [quality] Quality of the thumbnail, default is 100. Must be between 0 and 100. + /// - [png] Whether it will compress the thumbnail as PNG, otherwise as JPEG. + /// - [webp] Whether it will compress the thumbnail as WebP, otherwise as JPEG. + /// + /// [png] & [webp] can't be true at the same time. + /// + /// Returns thumbnail as [DocumentThumbnail] or null if thumbnail is not available. + /// Sometimes due to different document providers, thumbnail can have bigger dimensions, than requested. + /// Some document providers may not support thumbnail generation. + /// Added custom thumbnail generation for video & pdf & image files. + static Future fromUri( + String uri, { + int width = 256, + int height = 256, + int quality = 100, + bool png = false, + bool webp = false, + }) => + DocumentFile(uri: uri, exists: true, canThumbnail: true).thumbnail(); /// Converts the instance to a map. Map toMap() => { diff --git a/lib/src/extensions/document_file_ext.dart b/lib/src/extensions/document_file_ext.dart index bebaabc..06a9d6c 100644 --- a/lib/src/extensions/document_file_ext.dart +++ b/lib/src/extensions/document_file_ext.dart @@ -12,11 +12,13 @@ extension DocumentFileActionsExt on DocumentFile { /// /// `DocumentFile` [uri] must not be empty, before calling this method. /// - /// [uri] - can be `Uri` saved from previous request with persisted permission, + /// [uri] - can be `Content Uri` saved from previous request with persisted permission, /// or it can be app local `File.path` or `Directory.path`. /// /// **Note:** `Uri` without persisted permissions will not work, or uris like `content://media/external/file/106`. /// + /// **Note**: Or you can use static method `DocumentFile.fromUri(uri)` to get the `DocumentFile` instance. + /// /// Returns [DocumentFile] for the [uri], or null if uri is not valid. Future get() => DocumentFileMethods(this).get(); @@ -235,20 +237,22 @@ extension DocumentFileActivityExt on DocumentFile { /// [DocumentFile] must exist & must be a file, before calling this method. /// /// - [title] Title of the dialog to show when opening the document. - /// Title is used on Intent chooser dialog on Android, depends on os version & device. + /// Title is used on Intent chooser dialog on Android, depends on os version & device, + /// so it may not be shown on all devices. /// /// Returns `true` if the document is opened successfully, otherwise `false`. - Future open(String? title) => DocumentFileMethods(this).open(title); + Future open({String? title}) => DocumentFileMethods(this).open(title); /// Share the document with other apps. /// /// [DocumentFile] must exist & must be a file, before calling this method. /// /// - [title] Title of the dialog to show when sharing the document. - /// Title is used on Intent chooser dialog on Android, depends on os version & device. + /// Title is used on Intent chooser dialog on Android, depends on os version & device, + /// so it may not be shown on all devices. /// /// Returns `true` if the document is shared successfully, otherwise `false`. - Future share(String? title) => DocumentFileMethods(this).share(title); + Future share({String? title}) => DocumentFileMethods(this).share(title); /// Save the document to the selected location. ///