From 1c0e471458ba1b118f9227e53d3879ccba3c8c25 Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Fri, 11 Sep 2020 18:01:34 +0100 Subject: [PATCH] Adds iOS implementation, removes URI and isDirectory and adds withData optional property --- .../flutter/plugin/filepicker/FileInfo.java | 32 +----------- .../plugin/filepicker/FilePickerDelegate.java | 12 ++--- .../plugin/filepicker/FilePickerPlugin.java | 4 +- .../flutter/plugin/filepicker/FileUtils.java | 14 +++-- file_picker/example/android/build.gradle | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 7 +-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- file_picker/ios/Classes/FileInfo.h | 21 ++++++++ file_picker/ios/Classes/FileInfo.m | 34 ++++++++++++ file_picker/ios/Classes/FilePickerPlugin.m | 52 ++++++++++++------- file_picker/ios/Classes/FileUtils.h | 2 +- file_picker/ios/Classes/FileUtils.m | 26 +++++++--- file_picker/lib/src/file_picker.dart | 1 + file_picker/lib/src/file_picker_io.dart | 14 ++++- file_picker/lib/src/file_picker_web.dart | 3 +- file_picker/lib/src/platform_file.dart | 23 +++----- 16 files changed, 150 insertions(+), 99 deletions(-) create mode 100644 file_picker/ios/Classes/FileInfo.h create mode 100644 file_picker/ios/Classes/FileInfo.m diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java index 13ca187..6a639d3 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java @@ -6,38 +6,24 @@ import java.util.HashMap; public class FileInfo { - final Uri uri; final String path; final String name; final int size; final byte[] bytes; - final long lastModified; - final boolean isDirectory; - public FileInfo(Uri uri, String path, String name, int size, byte[] bytes, boolean isDirectory, long lastModified) { - this.uri = uri; + public FileInfo(String path, String name, int size, byte[] bytes) { this.path = path; this.name = name; this.size = size; this.bytes = bytes; - this.lastModified = lastModified; - this.isDirectory = isDirectory; } public static class Builder { - private Uri uri; private String path; private String name; private int size; - private long lastModified; private byte[] bytes; - private boolean isDirectory; - - public Builder withUri(Uri uri){ - this.uri = uri; - return this; - } public Builder withPath(String path){ this.path = path; @@ -59,32 +45,18 @@ public class FileInfo { return this; } - public Builder withDirectory(String path){ - this.path = path; - this.isDirectory = path != null; - return this; - } - - public Builder lastModifiedAt(long timeStamp){ - this.lastModified = timeStamp; - return this; - } - public FileInfo build() { - return new FileInfo(this.uri, this.path, this.name, this.size, this.bytes, this.isDirectory, this.lastModified); + return new FileInfo(this.path, this.name, this.size, this.bytes); } } public HashMap toMap() { final HashMap data = new HashMap<>(); - data.put("uri", uri.toString()); data.put("path", path); data.put("name", name); data.put("size", size); data.put("bytes", bytes); - data.put("isDirectory", isDirectory); - data.put("lastModified", lastModified); return data; } } diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java index fd8705f..703e183 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -17,10 +17,7 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import java.io.File; -import java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import io.flutter.plugin.common.EventChannel; @@ -36,6 +33,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private final PermissionManager permissionManager; private MethodChannel.Result pendingResult; private boolean isMultipleSelection = false; + private boolean loadDataToMemory = false; private String type; private String[] allowedExtensions; private EventChannel.EventSink eventSink; @@ -92,7 +90,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener int currentItem = 0; while (currentItem < count) { final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri(); - final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, currentUri); + final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, currentUri, loadDataToMemory); if(file != null) { files.add(file); @@ -121,7 +119,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener return; } - final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, uri); + final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, uri, loadDataToMemory); if(file != null) { files.add(file); @@ -223,7 +221,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener } @SuppressWarnings("deprecation") - public void startFileExplorer(final String type, final boolean isMultipleSelection, final String[] allowedExtensions, final MethodChannel.Result result) { + public void startFileExplorer(final String type, final boolean isMultipleSelection, final boolean withData, final String[] allowedExtensions, final MethodChannel.Result result) { if (!this.setPendingMethodCallAndResult(result)) { finishWithAlreadyActiveError(result); @@ -232,6 +230,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener this.type = type; this.isMultipleSelection = isMultipleSelection; + this.loadDataToMemory = withData; this.allowedExtensions = allowedExtensions; if (!this.permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) { @@ -242,6 +241,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener this.startFileExplorer(); } + @SuppressWarnings("unchecked") private void finishWithSuccess(Object data) { if (eventSink != null) { this.dispatchEventStatus(false); diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java index 4d9df23..e969996 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java @@ -112,6 +112,7 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte private MethodChannel channel; private static String fileType; private static boolean isMultipleSelection = false; + private static boolean withData = false; /** * Plugin registration. @@ -160,13 +161,14 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte result.notImplemented(); } else if (fileType != "dir") { isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); + withData = (boolean) arguments.get("withData"); allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); } if (fileType == "custom" && (allowedExtensions == null || allowedExtensions.length == 0)) { result.error(TAG, "Unsupported filter. Make sure that you are only using the extension without the dot, (ie., jpg instead of .jpg). This could also have happened because you are using an unsupported file extension. If the problem persists, you may want to consider using FileType.all instead.", null); } else { - this.delegate.startFileExplorer(fileType, isMultipleSelection, allowedExtensions, result); + this.delegate.startFileExplorer(fileType, isMultipleSelection, withData, allowedExtensions, result); } } diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java index 17643ea..41f302b 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java @@ -116,7 +116,7 @@ public class FileUtils { return true; } - public static FileInfo openFileStream(final Context context, final Uri uri) { + public static FileInfo openFileStream(final Context context, final Uri uri, boolean withData) { Log.i(TAG, "Caching from URI: " + uri.toString()); FileOutputStream fos = null; @@ -126,7 +126,7 @@ public class FileUtils { final File file = new File(path); - if(file.exists()) { + if(file.exists() && withData) { int size = (int) file.length(); byte[] bytes = new byte[size]; @@ -156,7 +156,9 @@ public class FileUtils { out.write(buffer, 0, len); } - fileInfo.withData(out.toByteArray()); + if(withData) { + fileInfo.withData(out.toByteArray()); + } out.writeTo(fos); out.flush(); } finally { @@ -177,11 +179,9 @@ public class FileUtils { Log.d(TAG, "File loaded and cached at:" + path); fileInfo - .lastModifiedAt(file.lastModified()) .withPath(path) .withName(fileName) - .withSize(Integer.parseInt(String.valueOf(file.length()/1024))) - .withUri(uri); + .withSize(Integer.parseInt(String.valueOf(file.length()/1024))); return fileInfo.build(); } @@ -195,8 +195,6 @@ public class FileUtils { String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con); FileInfo.Builder fileInfo = new FileInfo.Builder(); - fileInfo.withUri(treeUri); - if (volumePath == null) { return File.separator; } diff --git a/file_picker/example/android/build.gradle b/file_picker/example/android/build.gradle index e71893f..a61ccb1 100644 --- a/file_picker/example/android/build.gradle +++ b/file_picker/example/android/build.gradle @@ -17,7 +17,7 @@ allprojects { gradle.projectsEvaluated { tasks.withType(JavaCompile) { options.encoding = 'UTF-8' - options.compilerArgs << "-Xlint:deprecation" + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" } } } diff --git a/file_picker/example/ios/Runner.xcodeproj/project.pbxproj b/file_picker/example/ios/Runner.xcodeproj/project.pbxproj index 039ac22..e34ad1c 100644 --- a/file_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/file_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -164,7 +164,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1170; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -176,10 +176,9 @@ }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - English, en, Base, ); @@ -416,6 +415,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -443,6 +443,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; diff --git a/file_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/file_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 486f9c3..e74a32f 100644 --- a/file_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/file_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ * allowedExtensions; +@property (nonatomic) BOOL loadDataToMemory; @end @implementation FilePickerPlugin @@ -76,6 +77,8 @@ NSDictionary * arguments = call.arguments; BOOL isMultiplePick = ((NSNumber*)[arguments valueForKey:@"allowMultipleSelection"]).boolValue; + self.loadDataToMemory = ((NSNumber*)[arguments valueForKey:@"withData"]).boolValue; + if([call.method isEqualToString:@"any"] || [call.method containsString:@"custom"]) { self.allowedExtensions = [FileUtils resolveType:call.method withAllowedExtensions: [arguments valueForKey:@"allowedExtensions"]]; if(self.allowedExtensions == nil) { @@ -89,7 +92,7 @@ } else if([call.method isEqualToString:@"video"] || [call.method isEqualToString:@"image"] || [call.method isEqualToString:@"media"]) { [self resolvePickMedia:[FileUtils resolveMediaType:call.method] withMultiPick:isMultiplePick withCompressionAllowed:[arguments valueForKey:@"allowCompression"]]; } else if([call.method isEqualToString:@"audio"]) { - [self resolvePickAudio]; + [self resolvePickAudioWithMultiPick: isMultiplePick]; } else { result(FlutterMethodNotImplemented); _result = nil; @@ -223,38 +226,40 @@ // Did select [dkImagePickerController setDidSelectAssets:^(NSArray * __nonnull DKAssets) { - NSMutableArray* paths = [[NSMutableArray alloc] init]; + NSMutableArray* paths = [[NSMutableArray alloc] init]; for(DKAsset * asset in DKAssets){ - [paths addObject:asset.localTemporaryPath.path]; + [paths addObject:asset.localTemporaryPath.absoluteURL]; } - self->_result([paths count] > 0 ? paths : nil); - self->_result = nil; + [self handleResult: paths]; }]; [_viewController presentViewController:dkImagePickerController animated:YES completion:nil]; } -- (void) resolvePickAudio { +- (void) resolvePickAudioWithMultiPick:(BOOL)isMultiPick { self.audioPickerController = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAnyAudio]; self.audioPickerController.delegate = self; - self.audioPickerController.showsCloudItems = NO; - self.audioPickerController.allowsPickingMultipleItems = NO; + self.audioPickerController.showsCloudItems = YES; + self.audioPickerController.allowsPickingMultipleItems = isMultiPick; self.audioPickerController.modalPresentationStyle = UIModalPresentationCurrentContext; [self.viewController presentViewController:self.audioPickerController animated:YES completion:nil]; } +- (void) handleResult:(id) files { + _result([FileUtils resolveFileInfo: [files isKindOfClass: [NSArray class]] ? files : @[files] withData:self.loadDataToMemory]); + _result = nil; +} + #pragma mark - Delegates // DocumentPicker delegate - iOS 10 only - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url{ [self.documentPickerController dismissViewControllerAnimated:YES completion:nil]; - NSString * path = (NSString *)[url path]; - _result(@[path]); - _result = nil; + [self handleResult:url]; } // DocumentPicker delegate @@ -266,10 +271,14 @@ didPickDocumentsAtURLs:(NSArray *)urls{ } [self.documentPickerController dismissViewControllerAnimated:YES completion:nil]; - NSArray * result = [FileUtils resolvePath:urls]; - _result(result); - _result = nil; + if(controller.documentPickerMode == UIDocumentPickerModeOpen) { + _result([urls objectAtIndex:0].path); + _result = nil; + return; + } + + [self handleResult: urls]; } @@ -318,8 +327,7 @@ didPickDocumentsAtURLs:(NSArray *)urls{ return; } - _result(@[[pickedVideoUrl != nil ? pickedVideoUrl : pickedImageUrl path]]); - _result = nil; + [self handleResult: pickedVideoUrl != nil ? pickedVideoUrl : pickedImageUrl]; } @@ -327,12 +335,16 @@ didPickDocumentsAtURLs:(NSArray *)urls{ - (void)mediaPicker: (MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection { [mediaPicker dismissViewControllerAnimated:YES completion:NULL]; - NSURL *url = [[[mediaItemCollection items] objectAtIndex:0] valueForKey:MPMediaItemPropertyAssetURL]; - if(url == nil) { + NSMutableArray * urls = [[NSMutableArray alloc] initWithCapacity:[mediaItemCollection items].count]; + + for(MPMediaItemCollection * item in [mediaItemCollection items]) { + [urls addObject: [item valueForKey:MPMediaItemPropertyAssetURL]]; + } + + if(urls.count == 0) { Log(@"Couldn't retrieve the audio file path, either is not locally downloaded or the file is DRM protected."); } - _result([url absoluteString]); - _result = nil; + [self handleResult:urls]; } #pragma mark - Actions canceled diff --git a/file_picker/ios/Classes/FileUtils.h b/file_picker/ios/Classes/FileUtils.h index 5c9839d..a4f7b77 100644 --- a/file_picker/ios/Classes/FileUtils.h +++ b/file_picker/ios/Classes/FileUtils.h @@ -23,7 +23,7 @@ typedef NS_ENUM(NSInteger, MediaType) { + (BOOL) clearTemporaryFiles; + (NSArray*) resolveType:(NSString*)type withAllowedExtensions:(NSArray*)allowedExtensions; + (MediaType) resolveMediaType:(NSString*)type; -+ (NSArray*) resolvePath:(NSArray *)urls; ++ (NSArray*) resolveFileInfo:(NSArray *)urls withData:(BOOL)loadData; @end diff --git a/file_picker/ios/Classes/FileUtils.m b/file_picker/ios/Classes/FileUtils.m index fb59c9a..a25cf1b 100644 --- a/file_picker/ios/Classes/FileUtils.m +++ b/file_picker/ios/Classes/FileUtils.m @@ -6,11 +6,12 @@ // #import "FileUtils.h" +#import "FileInfo.h" @implementation FileUtils + (BOOL) clearTemporaryFiles { - NSString *tmpDirectory = NSTemporaryDirectory(); + NSString *tmpDirectory = NSTemporaryDirectory(); NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:tmpDirectory error:&error]; @@ -75,16 +76,25 @@ } } -+ (NSMutableArray*) resolvePath:(NSArray *)urls{ - NSString * uri; - NSMutableArray * paths = [[NSMutableArray alloc] init]; ++ (NSArray *)resolveFileInfo:(NSArray *)urls withData: (BOOL)loadData { - for (NSURL *url in urls) { - uri = (NSString *)[url path]; - [paths addObject:uri]; + if(urls == nil) { + return nil; } - return paths; + NSMutableArray * files = [[NSMutableArray alloc] initWithCapacity:urls.count]; + + for(NSURL * url in urls) { + NSString * path = (NSString *)[url path]; + NSDictionary * fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; + + [files addObject: [[[FileInfo alloc] initWithPath: path + andName: [[path lastPathComponent] stringByDeletingPathExtension] + andSize: [NSNumber numberWithLongLong: [@(fileAttributes.fileSize) longLongValue] / 1024] + andData: loadData ? [NSData dataWithContentsOfFile:path options: 0 error:nil] : nil] toData]]; + } + + return files; } @end diff --git a/file_picker/lib/src/file_picker.dart b/file_picker/lib/src/file_picker.dart index 1621ed9..d1646f2 100644 --- a/file_picker/lib/src/file_picker.dart +++ b/file_picker/lib/src/file_picker.dart @@ -55,6 +55,7 @@ abstract class FilePicker extends PlatformInterface { Function(FilePickerStatus) onFileLoading, bool allowCompression, bool allowMultiple = false, + bool withData, }) async => throw UnimplementedError('pickFiles() has not been implemented.'); diff --git a/file_picker/lib/src/file_picker_io.dart b/file_picker/lib/src/file_picker_io.dart index c33e1eb..d41860a 100644 --- a/file_picker/lib/src/file_picker_io.dart +++ b/file_picker/lib/src/file_picker_io.dart @@ -20,10 +20,18 @@ class FilePickerIO extends FilePicker { FileType type = FileType.any, List allowedExtensions, Function(FilePickerStatus) onFileLoading, - bool allowCompression, + bool allowCompression = true, bool allowMultiple = false, + bool withData = false, }) => - _getPath(type, allowMultiple, allowCompression, allowedExtensions, onFileLoading); + _getPath( + type, + allowMultiple, + allowCompression, + allowedExtensions, + onFileLoading, + withData, + ); @override Future clearTemporaryFiles() async => _channel.invokeMethod('clear'); @@ -47,6 +55,7 @@ class FilePickerIO extends FilePicker { bool allowCompression, List allowedExtensions, Function(FilePickerStatus) onFileLoading, + bool withData, ) async { final String type = describeEnum(fileType); if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) { @@ -65,6 +74,7 @@ class FilePickerIO extends FilePicker { 'allowMultipleSelection': allowMultipleSelection, 'allowedExtensions': allowedExtensions, 'allowCompression': allowCompression, + 'withData': withData, }); if (result == null) { diff --git a/file_picker/lib/src/file_picker_web.dart b/file_picker/lib/src/file_picker_web.dart index 013f0df..6d951a0 100644 --- a/file_picker/lib/src/file_picker_web.dart +++ b/file_picker/lib/src/file_picker_web.dart @@ -24,6 +24,7 @@ class FilePickerWeb extends FilePicker { bool allowMultiple = false, Function(FilePickerStatus) onFileLoading, bool allowCompression, + bool withData = true, }) async { final Completer> filesCompleter = Completer>(); @@ -48,7 +49,7 @@ class FilePickerWeb extends FilePicker { name: uploadInput.value.replaceAll('\\', '/'), path: uploadInput.value, size: bytes.length ~/ 1024, - bytes: bytes, + bytes: withData ? bytes : null, ), ); diff --git a/file_picker/lib/src/platform_file.dart b/file_picker/lib/src/platform_file.dart index fa82f81..435e14f 100644 --- a/file_picker/lib/src/platform_file.dart +++ b/file_picker/lib/src/platform_file.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; class PlatformFile { const PlatformFile({ this.path, - this.uri, this.name, this.bytes, this.size, @@ -12,28 +11,18 @@ class PlatformFile { PlatformFile.fromMap(Map data) : this.path = data['path'], - this.uri = data['uri'], this.name = data['name'], this.bytes = data['bytes'], this.size = data['size'], this.isDirectory = data['isDirectory']; - /// The absolute path for a cached copy of this file. - /// If you want to access the original file identifier use [uri] property instead. + /// The absolute path for a cached copy of this file. It can be used to create a + /// a file instance with a descriptor for the given path. + /// ``` + /// final File myFile = File(platformFile.path); + /// ``` final String path; - /// The URI (Universal Resource Identifier) for this file. - /// - /// This is the identifier of original resource and can be used to - /// manipulate the original file (read, write, delete). - /// - /// Android: it can be either content:// or file:// url. - /// - /// iOS: a file:// URL below a document provider (like iCloud). - /// - /// Web: Not supported, will be always `null`. - final String uri; - /// File name including its extension. final String name; @@ -48,5 +37,5 @@ class PlatformFile { final bool isDirectory; /// File extension for this file. - String get extension => name?.split('/')?.last; + String get extension => path?.split('.')?.last; }