diff --git a/README.md b/README.md index a3ae454..b62b67e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,121 @@ -# file_manager +# File Manager -A new Flutter project. +FileManager is a wonderful widget that allows you to manage files and folders, pick files and folders, and do a lot more. +Designed to feel like part of the Flutter framework. -## Getting Started -This project is a starting point for a Dart -[package](https://flutter.dev/developing-packages/), -a library module containing code that can be shared easily across -multiple Flutter or Dart projects. +## Usage -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Make sure to check out [examples](https://github.com/DevsOnFlutter/file_manager/blob/main/example/lib/main.dart) for more details. + +### Installation + +Add the following line to `pubspec.yaml`: + +```yaml +dependencies: + file_manager: ^1.0.0 +``` + +### Basic setup + +*The complete example is available [here](https://github.com/4-alok/draggable_home/blob/main/example/lib/main.dart).* + +Required parameter for **FileManager** are `controller` and `builder` +* `controller` The controller updates value and notifies its listeners, and FileManager updates itself appropriately whenever the user modifies the path or changes the sort-type with an associated FileManagerController. +``` +final FileManagerController controller = FileManagerController(); +``` +* `builder` This function allows you to create custom widgets and retrieve a list of entities `List.` + + + +Sample code +```dart +FileManager( + controller: controller, + builder: (context, snapshot) { + final List entitis = snapshot; + return ListView.builder( + itemCount: entitis.length, + itemBuilder: (context, index) { + return Card( + child: ListTile( + leading: isFile(entitis[index]) + ? Icon(Icons.feed_outlined) + : Icon(Icons.folder), + title: Text(basename(entitis[index])), + onTap: () { + if (isDirectory(entitis[index])) { + controller.openDirectory(entitis[index]); // open directory + } else { + // Perform file-related tasks. + } + }, + ), + ); + }, + ); + }, +), +``` + +## FileManager +| Properties | Description | +|--------------|-----------------| +| `loadingScreen` | For the loading screen, create a custom widget. A simple Centered CircularProgressIndicator is provided by default. | +| `emptyFolder` | For an empty screen, create a custom widget. | +| `controller` | For an empty screen, create a custom widget. | +| `hideHiddenEntity` | Hide the files and folders that are hidden. | +| `builder` | This function allows you to create custom widgets and retrieve a list of entities `List.` | + +## FileManagerContoller +| Properties | Description | +|--------------|-----------------| +| `getSortedBy` | The sorting type that is currently in use is returned. | +| `setSortedBy` | is used to set the sorting type. `SortBy{ name, type, date, size }`. | +| `getCurrentDirectory` | Get current Directory | +| `getCurrentPath` | Get current path, similar to [getCurrentDirectory]. | +| `setCurrentPath` | Set current directory path by providing `String` of path, similar to [openDirectory]. `List.` | +| `goToParentDirectory` | `goToParentDirectory` returns `bool`, goes to the parent directory of currently opened directory if the parent is accessible, return true if current directory is the root. false, if the current directory not on root of the stogare.. | +| `openDirectory` | Open directory by providing `Directory`. | + +## Otheres +| Properties | Description | +|--------------|-----------------| +| `isFile` | check weather FileSystemEntity is File. | +| `isDirectory` | check weather FileSystemEntity is Directory. | +| `basename` | Get the basename of Directory or File. Provide `File`, `Directory` or `FileSystemEntity` and returns the name as a `String`. ie +```dart + controller.dirName(dir); +``` +| +| `formatBytes` | Convert bytes to human readable size.[getCurrentDirectory]. | +| `setCurrentPath` | Set current directory path by providing `String` of path, similar to [openDirectory]. `List.` | +| `getStorageList` | Get list of available storage in the device, returns an empty list if there is no storage `List`| + +## Show some :heart: and :star: the repo + +[![GitHub followers](https://img.shields.io/github/followers/4-alok?style=social)](https://github.com/4-alok/) +[![GitHub followers](https://img.shields.io/github/stars/4-alok/draggable_home?style=social)](https://github.com/4-alok/) + +## Contributions + +Contributions are welcomed! + +If you feel that a hook is missing, feel free to open a pull-request. + +For a custom-hook to be merged, you will need to do the following: + +- Describe the use-case. + +- Open an issue explaining why we need this hook, how to use it, ... + This is important as a hook will not get merged if the hook doens't appeal to + a large number of people. + +- If your hook is rejected, don't worry! A rejection doesn't mean that it won't + be merged later in the future if more people shows an interest in it. + In the mean-time, feel free to publish your hook as a package on https://pub.dev. + +- A hook will not be merged unles fully tested, to avoid breaking it inadvertendly + in the future. \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 89f4496..8fae27f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:file_manager/file_manager.dart'; import 'package:flutter/material.dart'; @@ -26,13 +28,19 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { + // Creates a widget that registers a callback to veto attempts by the user to dismiss the enclosing + // or controllers the system's back button return WillPopScope( onWillPop: () async => (await controller.goToParentDirectory()), child: Scaffold( appBar: AppBar( actions: [ IconButton( - onPressed: () => controller.setCurrentStorage(storageIndex: 1), + onPressed: () => sort(context), + icon: Icon(Icons.sort_rounded), + ), + IconButton( + onPressed: () => selectStorage(context), icon: Icon(Icons.sd_storage_rounded), ) ], @@ -48,18 +56,50 @@ class HomePage extends StatelessWidget { child: FileManager( controller: controller, builder: (context, snapshot) { + final List entities = snapshot; return ListView.builder( - itemCount: snapshot.length, + itemCount: entities.length, itemBuilder: (context, index) { + FileSystemEntity entity = entities[index]; return Card( child: ListTile( - leading: isFile(snapshot[index]) + leading: isFile(entity) ? Icon(Icons.feed_outlined) : Icon(Icons.folder), - title: Text(basename(snapshot[index])), - onTap: () { - if (isDirectory(snapshot[index])) - controller.openDirectory(snapshot[index]); + title: Text(basename(entity)), + subtitle: subtitle(entity), + onTap: () async { + if (isDirectory(entity)) { + // open the folder + controller.openDirectory(entity); + + // delete a folder + // await entity.delete(recursive: true); + + // rename a folder + // await entity.rename("newPath"); + + // Check weather folder exists + // entity.exists(); + + // get date of file + // DateTime date = (await entity.stat()).modified; + } else { + // delete a file + // await entity.delete(); + + // rename a file + // await entity.rename("newPath"); + + // Check weather file exists + // entity.exists(); + + // get date of file + // DateTime date = (await entity.stat()).modified; + + // get the size of the file + // int size = (await entity.stat()).size; + } }, ), ); @@ -70,4 +110,100 @@ class HomePage extends StatelessWidget { )), ); } + + Widget subtitle(FileSystemEntity entity) { + return FutureBuilder( + future: entity.stat(), + builder: (context, snapshot) { + if (snapshot.hasData) { + if (entity is File) { + int size = snapshot.data!.size; + + return Text( + "${formatBytes(size)}", + ); + } + return Text( + "${snapshot.data!.modified}", + ); + } else { + return Text(""); + } + }, + ); + } + + selectStorage(BuildContext context) { + showDialog( + context: context, + builder: (context) => Dialog( + child: FutureBuilder>( + future: getStorageList(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final List storageList = snapshot.data!; + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: storageList + .map((e) => ListTile( + title: Text( + "${basename(e)}", + ), + onTap: () { + controller.openDirectory(e); + Navigator.pop(context); + }, + )) + .toList()), + ); + } + return Dialog( + child: CircularProgressIndicator(), + ); + }, + ), + ), + ); + } + + sort(BuildContext context) async { + showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + padding: EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Name"), + onTap: () { + controller.sortedBy = SortBy.name; + Navigator.pop(context); + }), + ListTile( + title: Text("Size"), + onTap: () { + controller.sortedBy = SortBy.size; + Navigator.pop(context); + }), + ListTile( + title: Text("Date"), + onTap: () { + controller.sortedBy = SortBy.date; + Navigator.pop(context); + }), + ListTile( + title: Text("type"), + onTap: () { + controller.sortedBy = SortBy.type; + Navigator.pop(context); + }), + ], + ), + ), + )); + } } diff --git a/lib/controller/file_manager_controller.dart b/lib/controller/file_manager_controller.dart index cdcf269..72b97c0 100644 --- a/lib/controller/file_manager_controller.dart +++ b/lib/controller/file_manager_controller.dart @@ -4,37 +4,35 @@ import 'package:flutter/material.dart'; class FileManagerController extends ChangeNotifier { String _path = ""; - int _currentStorage = 0; SortBy _short = SortBy.size; - // TODO: [Documentation] - - /// getSorted by returns the current sorting type of the FileManager + /// The sorting type that is currently in use is returned. SortBy get getSortedBy => _short; - // TODO: [Documentation] - /// setSortedBy is used to set the sorting type. - set setSortedBy(SortBy sortType) { + + /// [setSortedBy] is used to set the sorting type. + /// + /// `SortBy{ name, type, date, size }` + set sortedBy(SortBy sortType) { _short = sortType; notifyListeners(); } - /// Get current directory path. + /// Get current Directory. Directory get getCurrentDirectory => Directory(_path); - String get getCurrentPath { - return _path; - } - /// Set current directory path by providing string of path. + /// Get current path, similar to [getCurrentDirectory]. + String get getCurrentPath => _path; + + /// Set current directory path by providing string of path, similar to [openDirectory]. set setCurrentPath(String path) { _path = path; notifyListeners(); } - // TODO: [Documentation] - /// goToParentDirectory returns false and goes to the parent directory of currently opened directory if the parent is accessible, - /// it will return true and pops the screen if the parent of currently opened directory is not accessible. + /// [goToParentDirectory] returns [bool], goes to the parent directory of currently opened directory if the parent is accessible, + /// return true if current directory is the root. false, if the current directory not on root of the stogare. Future goToParentDirectory() async { - List storageList = (await getStorageList())!; + List storageList = (await getStorageList()); final bool willNotGoToParent = (storageList .where((element) => element.path == Directory(_path).path) .isNotEmpty); @@ -42,24 +40,13 @@ class FileManagerController extends ChangeNotifier { return willNotGoToParent; } - /// Open directory by providing Directory. + /// Open directory by providing [Directory]. void openDirectory(FileSystemEntity entity) { if (entity is Directory) { _path = entity.path; notifyListeners(); } else { - print( - "FileSystemEntity entity is File. Please provide a Directory(folder) to be opened not File"); + throw ("FileSystemEntity entity is File. Please provide a Directory(folder) to be opened not File"); } } - - /// Get current storege. ie: 0 is for internal storage. 1, 2 and so on, if any external storage is available. - int get getCurrentStorage => _currentStorage; - - /// Set current storege. ie: 0 is for internal storage. 1, 2 and so on, if any external storage is available. - Future setCurrentStorage({required int storageIndex}) async { - _currentStorage = storageIndex; - _path = (await getStorageList())![storageIndex].path; - notifyListeners(); - } } diff --git a/lib/file_manager.dart b/lib/file_manager.dart index b7e6d89..1bd76b8 100644 --- a/lib/file_manager.dart +++ b/lib/file_manager.dart @@ -1,10 +1,10 @@ library file_manager; import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; -// Library Imports import 'package:file_manager/helper/helper.dart'; export 'package:file_manager/helper/helper.dart'; @@ -19,8 +19,6 @@ class _PathStat { _PathStat(this.path, this.dateTime); } -///sort the files and folders according to the SortBy type -///It will return a list of fileSystemEntity after sorting Future> _sortEntitysList( String path, SortBy sortType) async { final List list = await Directory(path).list().toList(); @@ -99,7 +97,7 @@ Future> _sortEntitysList( } /// check weather FileSystemEntity is File -/// return true if FileSystemEntity is a File else returns false +/// return true if FileSystemEntity is File else returns false bool isFile(FileSystemEntity entity) { return (entity is File); } @@ -111,8 +109,13 @@ bool isDirectory(FileSystemEntity entity) { } /// Get the basename of Directory or File. -/// takes the directory and returns the name of file or folder -/// ie: controller.dirName(dir); +/// +/// Provide [File], [Directory] or [FileSystemEntity] and returns the name as a [String]. +/// +/// ie: +/// ```dart +/// controller.dirName(dir); +/// ``` /// to hide the extension of file, showFileExtension = flase String basename(dynamic entity, [bool showFileExtension = true]) { if (entity is Directory) { @@ -128,9 +131,21 @@ String basename(dynamic entity, [bool showFileExtension = true]) { } } +/// Convert bytes to human readable size +String formatBytes(int bytes, [precision = 2]) { + if (bytes != 0) { + final double base = math.log(bytes) / math.log(1024); + final suffix = const ['B', 'KB', 'MB', 'GB', 'TB'][base.floor()]; + final size = math.pow(1024, base - base.floor()); + return '${size.toStringAsFixed(2)} $suffix'; + } else { + return "0B"; + } +} + /// Get list of available storage in the device /// returns an empty list if there is no storage -Future?> getStorageList() async { +Future> getStorageList() async { if (Platform.isAndroid) { List storages = (await getExternalStorageDirectories())!; storages = storages.map((Directory e) { @@ -153,20 +168,51 @@ Future?> getStorageList() async { return []; } +/// FileManager is a wonderful widget that allows you to manage files and folders, pick files and folders, and do a lot more. +/// Designed to feel like part of the Flutter framework. +/// +/// Sample code +///```dart +///FileManager( +/// controller: controller, +/// builder: (context, snapshot) { +/// final List entitis = snapshot; +/// return ListView.builder( +/// itemCount: entitis.length, +/// itemBuilder: (context, index) { +/// return Card( +/// child: ListTile( +/// leading: isFile(entitis[index]) +/// ? Icon(Icons.feed_outlined) +/// : Icon(Icons.folder), +/// title: Text(basename(entitis[index])), +/// onTap: () { +/// if (isDirectory(entitis[index])) { +/// controller +/// .openDirectory(entitis[index]); +/// } else { +/// // Perform file-related tasks. +/// } +/// }, +/// ), +/// ); +/// }, +/// ); +/// }, +///), +///``` class FileManager extends StatefulWidget { - /// Provide a custom widget for loading screen. - /// as default CircularProgressIndicator is provided. + /// For the loading screen, create a custom widget. + /// Simple Centered CircularProgressIndicator is provided by default. final Widget? loadingScreen; - /// Provide a scroll Physics for scrolling behaviour. - final ScrollPhysics? physics; - - ///shrinkwrap will only occupy the space it need - final bool shrinkWrap; + /// For an empty screen, create a custom widget. + final Widget? emptyFolder; + ///Controls the state of the FileManager. final FileManagerController controller; - ///Builder is a custom builder which takes an entity and bulds a widget around it + ///This function allows you to create custom widgets and retrieve a list of entities `List.` /// /// ///``` @@ -192,13 +238,12 @@ class FileManager extends StatefulWidget { /// ``` final _Builder builder; - /// Hide the hidden file and folder. + /// Hide the files and folders that are hidden. final bool hideHiddenEntity; FileManager({ + this.emptyFolder, this.loadingScreen, - this.physics, - this.shrinkWrap = false, required this.controller, required this.builder, this.hideHiddenEntity = true, @@ -216,14 +261,12 @@ class _FileManagerState extends State { void initState() { super.initState(); - /// add the listner to the contoller widget.controller.addListener(() { path.value = widget.controller.getCurrentPath; sort.value = widget.controller.getSortedBy; }); } - /// dispose all the value notifiers @override void dispose() { path.dispose(); @@ -237,20 +280,19 @@ class _FileManagerState extends State { future: getStorageList(), builder: (context, snapshot) { if (snapshot.hasData) { - widget.controller.setCurrentPath = snapshot.data![0].path; - return body(context); + widget.controller.setCurrentPath = snapshot.data!.first.path; + return _body(context); } else if (snapshot.hasError) { print(snapshot.error); - return errorPage(snapshot.error.toString()); + return _errorPage(snapshot.error.toString()); } else { - return loadingScreenWidget(); + return _loadingScreenWidget(); } }, ); } - /// main body of File Manager can fetch data from the device - Widget body(BuildContext context) { + Widget _body(BuildContext context) { return ValueListenableBuilder( valueListenable: path, builder: (context, pathSnapshot, _) { @@ -262,6 +304,9 @@ class _FileManagerState extends State { builder: (context, snapshot) { if (snapshot.hasData) { List entitys = snapshot.data!; + if (entitys.length == 0) { + return _emptyFolderWidger(); + } if (widget.hideHiddenEntity) { entitys = entitys.where((element) { if (basename(element) == "" || @@ -275,9 +320,9 @@ class _FileManagerState extends State { return widget.builder(context, entitys); } else if (snapshot.hasError) { print(snapshot.error); - return errorPage(snapshot.error.toString()); + return _errorPage(snapshot.error.toString()); } else { - return loadingScreenWidget(); + return _loadingScreenWidget(); } }); }); @@ -285,8 +330,16 @@ class _FileManagerState extends State { ); } - /// error page that displays the error in the screen - Container errorPage(String error) { + Widget _emptyFolderWidger() { + if (widget.emptyFolder == null) { + return Container( + child: Center(child: Text("Empty Directory")), + ); + } else + return widget.emptyFolder!; + } + + Container _errorPage(String error) { return Container( color: Colors.red, child: Center( @@ -295,9 +348,7 @@ class _FileManagerState extends State { ); } - /// loading screen if the backend is currently fecthing data from the device. - /// It will display simple Circular Progress Indicator if no custom widget is passed. - Widget loadingScreenWidget() { + Widget _loadingScreenWidget() { if ((widget.loadingScreen == null)) { return Container( child: Center(