diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 3aba78e..ac85ea7 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"permission_handler_apple","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler_apple-9.0.7\\\\","native_build":true,"dependencies":[]},{"name":"photo_gallery","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\photo_gallery-1.1.1\\\\","native_build":true,"dependencies":[]},{"name":"video_player_avfoundation","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_player_avfoundation-2.3.8\\\\","native_build":true,"dependencies":[]},{"name":"video_thumbnail","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_thumbnail-0.5.3\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"permission_handler_android","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler_android-10.2.0\\\\","native_build":true,"dependencies":[]},{"name":"photo_gallery","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\photo_gallery-1.1.1\\\\","native_build":true,"dependencies":[]},{"name":"video_player_android","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_player_android-2.3.10\\\\","native_build":true,"dependencies":[]},{"name":"video_thumbnail","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_thumbnail-0.5.3\\\\","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[{"name":"permission_handler_windows","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler_windows-0.1.2\\\\","native_build":true,"dependencies":[]}],"web":[{"name":"video_player_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_player_web-2.0.13\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"photo_gallery","dependencies":[]},{"name":"video_player","dependencies":["video_player_android","video_player_avfoundation","video_player_web"]},{"name":"video_player_android","dependencies":[]},{"name":"video_player_avfoundation","dependencies":[]},{"name":"video_player_web","dependencies":[]},{"name":"video_thumbnail","dependencies":[]}],"date_created":"2023-01-06 16:12:29.754813","version":"3.3.10"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"permission_handler_apple","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler_apple-9.0.7\\\\","native_build":true,"dependencies":[]},{"name":"photo_gallery","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\photo_gallery-1.1.1\\\\","native_build":true,"dependencies":[]},{"name":"video_player_avfoundation","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_player_avfoundation-2.3.8\\\\","native_build":true,"dependencies":[]},{"name":"video_thumbnail","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_thumbnail-0.5.3\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"permission_handler_android","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler_android-10.2.0\\\\","native_build":true,"dependencies":[]},{"name":"photo_gallery","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\photo_gallery-1.1.1\\\\","native_build":true,"dependencies":[]},{"name":"video_player_android","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_player_android-2.3.10\\\\","native_build":true,"dependencies":[]},{"name":"video_thumbnail","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_thumbnail-0.5.3\\\\","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[{"name":"permission_handler_windows","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler_windows-0.1.2\\\\","native_build":true,"dependencies":[]}],"web":[{"name":"video_player_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\video_player_web-2.0.13\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"photo_gallery","dependencies":[]},{"name":"video_player","dependencies":["video_player_android","video_player_avfoundation","video_player_web"]},{"name":"video_player_android","dependencies":[]},{"name":"video_player_avfoundation","dependencies":[]},{"name":"video_player_web","dependencies":[]},{"name":"video_thumbnail","dependencies":[]}],"date_created":"2023-01-20 09:57:50.239997","version":"3.3.10"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b7189e8..4f2cf2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,4 +93,10 @@ ## 0.2.3 -* thumbnail's resolotion upgraded \ No newline at end of file +* thumbnail's resolotion upgraded + +## 0.3.0 + +* Package performance has been improved +* BottomSheetLayout changed into PickerScaffold +* PermissionDeniedPage added \ No newline at end of file diff --git a/README.md b/README.md index 2d0b15a..e4b0c5a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Gallery Picker is a flutter package that will allow you to pick media file(s), manage and navigate inside your gallery with modern tools and views. - + ## Features @@ -32,6 +32,8 @@ Gallery Picker is a flutter package that will allow you to pick media file(s), m [✔] Examples provided (example/lib/examples) +[✔] Permission requests handled within the library + [✔] Null-safety You could find the code samples of the given gifs below in `/example/lib/examples` folder. @@ -40,16 +42,16 @@ You could find the code samples of the given gifs below in `/example/lib/example
- + - + - + - +
@@ -107,24 +109,23 @@ Dispose listener GalleryPicker.disposeSelectedFilesListener(); ``` -### BottomSheetLayout +### PickerScaffold -Gallery Picker could also work as a bottom sheet. Wrap your scaffold's body with BottomSheetLayout +Gallery Picker could also work as a bottom sheet. Use PickerScaffold instead your Scaffold. There is an example at `example/lib/examples/bottom_sheet_example.dart` to see how it could be done. ```dart @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: BottomSheetLayout( - config: Config() - onSelect: (media) {}, - child: Column( - children: [ + return PickerScaffold( + backgroundColor: Colors.transparent, + onSelect: (media) {}, + initSelectedMedia: initMedia, + config: Config(mode: Mode.dark), + body: Container(), + ) + } ``` ### Customizable destination page @@ -225,6 +226,7 @@ List? media = await GalleryPicker.pickMedia( context: context, config: Config( backgroundColor: Colors.white, + permissionDeniedPage:PermissionDeniedPage(), appbarColor: Colors.white, bottomSheetColor: const Color.fromARGB(255, 247, 248, 250), appbarIconColor: const Color.fromARGB(255, 130, 141, 148), @@ -326,6 +328,19 @@ GalleryPicker returns MediaFile list. You can reach out features below. [✔] getData function [✔] Check if the file selected in gallery picker +## Permission +Required permissions will be requested when gallery picker is launched. In case of user's rejection of request, the problem will be handled within gallery picker package. + + + +### Customizing Permission Denied Page + +```dart +Config( + permissionDeniedPage: PermissionDeniedPage(), +) +``` + ## Ready-to-use widgets ### ThumbnailMedia @@ -413,6 +428,44 @@ AlbumCategoriesView( ) ``` +## Breaking Changes From 0.2.3 + +### BottomSheetLayout changed into PickerScaffold + +Before: + +```dart + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: BottomSheetLayout( + config: Config() + onSelect: (media) {}, + child: Column( + children: [ +``` + +Now: + +```dart + @override + Widget build(BuildContext context) { + return PickerScaffold( + backgroundColor: Colors.transparent, + onSelect: (media) {}, + initSelectedMedia: initMedia, + config: Config(mode: Mode.dark), + body: Container(), + ) + } +``` + + + + ## Examples Check out our examples! ### Standart Gallery Picker diff --git a/example/lib/examples/bottom_sheet_example.dart b/example/lib/examples/bottom_sheet_example.dart index 6ee7083..3eb7759 100644 --- a/example/lib/examples/bottom_sheet_example.dart +++ b/example/lib/examples/bottom_sheet_example.dart @@ -15,136 +15,135 @@ class _BottomSheetExampleState extends State { var controller = PageController(initialPage: 0); @override Widget build(BuildContext context) { - return Scaffold( + return PickerScaffold( backgroundColor: Colors.white, - body: BottomSheetLayout( - onSelect: (List selectedMedias) { - this.selectedMedias = selectedMedias; - pageIndex = 0; - if (this.selectedMedias.isNotEmpty) { - Future.delayed(const Duration(milliseconds: 500)).then((value) { - controller.animateToPage(0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeIn); - }); - } - setState(() {}); - }, - config: Config(mode: Mode.dark), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - const Text( - 'These are your selected medias', - ), - const Divider(), - Expanded( - flex: 5, - child: Stack(children: [ - if (selectedMedias.isNotEmpty) - PageView( - controller: controller, - children: [ - for (var media in selectedMedias) - Center( - child: MediaProvider( - media: media, - ), - ) - ], - ), - if (selectedMedias.isNotEmpty) - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - if (pageIndex < selectedMedias.length - 1) { - pageIndex++; - controller.animateToPage(pageIndex, - duration: const Duration(milliseconds: 500), - curve: Curves.easeIn); - setState(() {}); - } - }, - child: const Icon( - Icons.chevron_right, - size: 100, - color: Colors.red, - )), - ), - if (selectedMedias.isNotEmpty) - Align( - alignment: Alignment.centerLeft, - child: TextButton( - onPressed: () { - if (pageIndex > 0) { - pageIndex--; - controller.animateToPage(pageIndex, - duration: const Duration(milliseconds: 500), - curve: Curves.easeIn); - setState(() {}); - } - }, - child: const Icon( - Icons.chevron_left, - size: 100, - color: Colors.red, - )), - ), - ]), - ), - SizedBox( - height: 65, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - for (int i = 0; i < selectedMedias.length; i++) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: TextButton( - onPressed: () { - pageIndex = i; + onSelect: (List selectedMedias) { + this.selectedMedias = selectedMedias; + pageIndex = 0; + if (this.selectedMedias.isNotEmpty) { + Future.delayed(const Duration(milliseconds: 500)).then((value) { + controller.animateToPage(0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn); + }); + } + setState(() {}); + }, + config: Config(mode: Mode.dark), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + const Text( + 'These are your selected medias', + style: TextStyle(color: Colors.black), + ), + const Divider(), + Expanded( + flex: 5, + child: Stack(children: [ + if (selectedMedias.isNotEmpty) + PageView( + controller: controller, + children: [ + for (var media in selectedMedias) + Center( + child: MediaProvider( + media: media, + ), + ) + ], + ), + if (selectedMedias.isNotEmpty) + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + if (pageIndex < selectedMedias.length - 1) { + pageIndex++; controller.animateToPage(pageIndex, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); setState(() {}); - }, - child: Container( - width: 65, - height: 50, - decoration: BoxDecoration( - border: Border.all( - width: 2, - color: pageIndex == i - ? Colors.red - : Colors.black)), - child: ThumbnailMedia( - media: selectedMedias[i], - )), - ), - ) - ], - ), + } + }, + child: const Icon( + Icons.chevron_right, + size: 100, + color: Colors.red, + )), + ), + if (selectedMedias.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: () { + if (pageIndex > 0) { + pageIndex--; + controller.animateToPage(pageIndex, + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn); + setState(() {}); + } + }, + child: const Icon( + Icons.chevron_left, + size: 100, + color: Colors.red, + )), + ), + ]), + ), + SizedBox( + height: 65, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (int i = 0; i < selectedMedias.length; i++) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: TextButton( + onPressed: () { + pageIndex = i; + controller.animateToPage(pageIndex, + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn); + setState(() {}); + }, + child: Container( + width: 65, + height: 50, + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: pageIndex == i + ? Colors.red + : Colors.black)), + child: ThumbnailMedia( + media: selectedMedias[i], + )), + ), + ) + ], ), - const Spacer( - flex: 1, + ), + const Spacer( + flex: 1, + ), + TextButton( + onPressed: () { + GalleryPicker.openSheet(); + }, + child: const Icon( + Icons.open_in_new, + size: 40, ), - TextButton( - onPressed: () { - GalleryPicker.openSheet(); - }, - child: const Icon( - Icons.open_in_new, - size: 40, - ), - ), - const Spacer( - flex: 1, - ), - ], - ), + ), + const Spacer( + flex: 1, + ), + ], ), ), ); diff --git a/example/lib/examples/whatsapp_pick_photo.dart b/example/lib/examples/whatsapp_pick_photo.dart index e7304f1..113ca7f 100644 --- a/example/lib/examples/whatsapp_pick_photo.dart +++ b/example/lib/examples/whatsapp_pick_photo.dart @@ -17,20 +17,22 @@ class _WhatsappPickPhotoState extends State { GalleryMedia? gallery; List selectedMedias = []; List? cameras; - CameraLensDirection cameraLensDirection = CameraLensDirection.front; + CameraLensDirection cameraLensDirection = CameraLensDirection.back; @override void initState() { initCamera(); fetchMedias(); GalleryPicker.listenSelectedFiles.listen((medias) { selectedMedias = medias; - setState(() {}); + if (mounted) { + setState(() {}); + } }); super.initState(); } Future fetchMedias() async { - gallery = await GalleryPicker.collectGallery; + gallery = await GalleryPicker.initializeGallery; setState(() {}); } @@ -72,211 +74,207 @@ class _WhatsappPickPhotoState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return PickerScaffold( backgroundColor: Colors.transparent, - body: BottomSheetLayout( - onSelect: (List selectedMedias) { - this.selectedMedias = selectedMedias; + onSelect: (List selectedMedias) { + this.selectedMedias = selectedMedias; + if (mounted) { setState(() {}); - }, - initSelectedMedia: selectedMedias, - config: Config(mode: Mode.dark), - child: Stack( - children: [ - if (cameraController != null && - cameraController!.value.isInitialized) - SizedBox( - height: MediaQuery.of(context).size.height, - child: CameraPreview( - cameraController!, - ), + } + }, + initSelectedMedia: selectedMedias, + config: Config(mode: Mode.dark), + body: Stack( + children: [ + if (cameraController != null && cameraController!.value.isInitialized) + SizedBox( + height: MediaQuery.of(context).size.height, + child: CameraPreview( + cameraController!, ), - if (gallery != null && gallery!.recent != null) - Positioned( - bottom: 100, - child: SizedBox( - width: MediaQuery.of(context).size.width, - height: 65, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - for (var media in gallery!.recent!.files) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: GestureDetector( - onTap: () { - if (selectedMedias.isEmpty) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MultipleMediasView([media])), - ); - } else { - selectedMedias.any( - (element) => element.id == media.id) - ? selectedMedias.removeWhere( - (element) => element.id == media.id) - : selectedMedias.add(media); - setState(() {}); - } - }, - onLongPress: () { - if (selectedMedias - .any((element) => element.id == media.id)) { - selectedMedias.removeWhere( - (element) => element.id == media.id); - } else { - selectedMedias.add(media); - } - setState(() {}); - }, - child: Container( - width: 65, - height: 65, - decoration: BoxDecoration( - border: Border.all( - width: 2, - color: selectedMedias.any((element) => - element.id == media.id) - ? Colors.red - : Colors.black)), - child: Stack( - children: [ - SizedBox( - width: 65, - height: 65, - child: ThumbnailMedia( - media: media, - ), - ), - if (selectedMedias.any( - (element) => element.id == media.id)) - Container( - color: Colors.black.withOpacity(0.3), - alignment: Alignment.center, - child: const Icon( - Icons.check, - size: 30, - color: Colors.white, - ), - ) - ], - )), - ), - ) - ], - ), - ), - ), - if (selectedMedias.isNotEmpty) - Positioned( - bottom: 150, - right: 10, - child: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MultipleMediasView(selectedMedias)), - ); - }, - mini: true, - child: const Icon( - Icons.check, - color: Colors.white, - ), - )), + ), + if (gallery != null && gallery!.recent != null) Positioned( - bottom: 20, - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TextButton( - onPressed: () { - GalleryPicker.openSheet(); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle), - padding: const EdgeInsets.all(8), - child: const Icon( - Icons.image, - size: 20, - color: Colors.white, - ), - ), - ), - GestureDetector( - onTap: () async { - setState(() { - anyProcess = true; - }); - Future.delayed(const Duration(milliseconds: 100)) - .then((value) => setState(() { - anyProcess = false; - })); - }, - onLongPressStart: (value) { - setState(() { - isRecording = true; - anyProcess = true; - }); - }, - onLongPressEnd: (value) async { - setState(() { - isRecording = false; - anyProcess = false; - }); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: - Border.all(width: 2, color: Colors.white)), - width: isRecording ? 80 : 65, - height: isRecording ? 80 : 65, + bottom: 100, + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: 65, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (var media in gallery!.recent!.files) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: GestureDetector( + onTap: () { + if (selectedMedias.isEmpty) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MultipleMediasView([media])), + ); + } else { + selectedMedias + .any((element) => element.id == media.id) + ? selectedMedias.removeWhere( + (element) => element.id == media.id) + : selectedMedias.add(media); + setState(() {}); + } + }, + onLongPress: () { + if (selectedMedias + .any((element) => element.id == media.id)) { + selectedMedias.removeWhere( + (element) => element.id == media.id); + } else { + selectedMedias.add(media); + } + setState(() {}); + }, child: Container( - decoration: BoxDecoration( - color: - anyProcess ? Colors.red : Colors.transparent, - shape: BoxShape.circle, - ), - ), + width: 65, + height: 65, + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: selectedMedias.any((element) => + element.id == media.id) + ? Colors.red + : Colors.black)), + child: Stack( + children: [ + SizedBox( + width: 65, + height: 65, + child: ThumbnailMedia( + media: media, + ), + ), + if (selectedMedias + .any((element) => element.id == media.id)) + Container( + color: Colors.black.withOpacity(0.3), + alignment: Alignment.center, + child: const Icon( + Icons.check, + size: 30, + color: Colors.white, + ), + ) + ], + )), + ), + ) + ], + ), + ), + ), + if (selectedMedias.isNotEmpty) + Positioned( + bottom: 150, + right: 10, + child: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MultipleMediasView(selectedMedias)), + ); + }, + mini: true, + child: const Icon( + Icons.check, + color: Colors.white, + ), + )), + Positioned( + bottom: 20, + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton( + onPressed: () { + GalleryPicker.openSheet(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle), + padding: const EdgeInsets.all(8), + child: const Icon( + Icons.image, + size: 20, + color: Colors.white, ), ), - TextButton( - onPressed: () { - if (cameraLensDirection == - CameraLensDirection.front) { - cameraLensDirection = CameraLensDirection.back; - } else { - cameraLensDirection = CameraLensDirection.front; - } - initCamera(); - }, + ), + GestureDetector( + onTap: () async { + setState(() { + anyProcess = true; + }); + Future.delayed(const Duration(milliseconds: 100)) + .then((value) => setState(() { + anyProcess = false; + })); + }, + onLongPressStart: (value) { + setState(() { + isRecording = true; + anyProcess = true; + }); + }, + onLongPressEnd: (value) async { + setState(() { + isRecording = false; + anyProcess = false; + }); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(width: 2, color: Colors.white)), + width: isRecording ? 80 : 65, + height: isRecording ? 80 : 65, child: Container( decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle), - padding: const EdgeInsets.all(8), - child: const Icon( - Icons.cameraswitch, - size: 20, - color: Colors.white, + color: anyProcess ? Colors.red : Colors.transparent, + shape: BoxShape.circle, ), ), ), - ], - ), - )) - ], - ), + ), + TextButton( + onPressed: () { + if (cameraLensDirection == CameraLensDirection.front) { + cameraLensDirection = CameraLensDirection.back; + } else { + cameraLensDirection = CameraLensDirection.front; + } + initCamera(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle), + padding: const EdgeInsets.all(8), + child: const Icon( + Icons.cameraswitch, + size: 20, + color: Colors.white, + ), + ), + ), + ], + ), + )) + ], ), ); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 302d916..ae2f9c6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -15,13 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" - bottom_sheet_bar: + bottom_sheet_scaffold: dependency: transitive description: - name: bottom_sheet_bar + name: bottom_sheet_scaffold url: "https://pub.dartlang.org" source: hosted - version: "2.3.8" + version: "0.1.1" camera: dependency: "direct dev" description: @@ -141,7 +141,7 @@ packages: path: ".." relative: true source: path - version: "0.2.3" + version: "0.3.0" get: dependency: transitive description: @@ -191,13 +191,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.5" - measure_size: - dependency: transitive - description: - name: measure_size - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" meta: dependency: transitive description: @@ -205,6 +198,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + page_transition: + dependency: transitive + description: + name: page_transition + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" path: dependency: transitive description: @@ -379,5 +379,5 @@ packages: source: hosted version: "0.5.3" sdks: - dart: ">=2.18.5 <3.0.0" + dart: ">=2.18.6 <3.0.0" flutter: ">=3.0.0" diff --git a/lib/controller/bottom_sheet_controller.dart b/lib/controller/bottom_sheet_controller.dart deleted file mode 100644 index 53691fc..0000000 --- a/lib/controller/bottom_sheet_controller.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:bottom_sheet_bar/bottom_sheet_bar.dart'; -import 'package:get/get.dart'; - -import 'gallery_controller.dart'; - -class BottomSheetController extends GetxController { - BottomSheetBarController sheetController; - PhoneGalleryController? galleryController; - bool isClosing = false; - bool appBarTapping = false; - - Future open() async { - await sheetController.expand(); - } - - void tapingStatus(bool value) { - appBarTapping = value; - if (galleryController == null) { - Get.find().update(); - } else { - galleryController!.update(); - } - update(); - } - - Future close() async { - isClosing = true; - update(); - await sheetController.collapse(); - isClosing = false; - update(); - } - - void disposeController() { - isClosing = false; - appBarTapping = false; - GetInstance().delete(); - } - - BottomSheetController(this.sheetController); -} diff --git a/lib/controller/gallery_controller.dart b/lib/controller/gallery_controller.dart index 3c807da..8d71867 100644 --- a/lib/controller/gallery_controller.dart +++ b/lib/controller/gallery_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -15,13 +16,22 @@ import 'picker_listener.dart'; class PhoneGalleryController extends GetxController { late Config config; - PhoneGalleryController(Config? config, - {required this.onSelect, - required this.heroBuilder, - required this.isRecent, + void configuration(Config? config, + {required dynamic Function(List) onSelect, + required Widget Function(String, MediaFile, BuildContext)? heroBuilder, + required bool isRecent, + required bool startWithRecent, required List? initSelectedMedias, required List? extraRecentMedia, - required this.multipleMediasBuilder}) { + required Widget Function(List, BuildContext)? + multipleMediasBuilder}) { + this.onSelect = onSelect; + this.heroBuilder = heroBuilder; + this.isRecent = isRecent; + this.startWithRecent = startWithRecent; + this.multipleMediasBuilder = multipleMediasBuilder; + pageController = PageController(); + pickerPageController = PageController(initialPage: startWithRecent ? 0 : 1); this.config = config ?? Config(); if (initSelectedMedias != null) { _selectedFiles = initSelectedMedias.map((e) => e).toList(); @@ -32,16 +42,21 @@ class PhoneGalleryController extends GetxController { if (selectedFiles.isNotEmpty) { _pickerMode = true; } + configurationCompleted = true; } - bool isRecent; - Function(List selectedMedias) onSelect; + + late bool startWithRecent; + late bool isRecent; + bool permissionGranted = false; + bool configurationCompleted = false; + late Function(List selectedMedias) onSelect; Widget Function(String tag, MediaFile media, BuildContext context)? heroBuilder; Widget Function(List medias, BuildContext context)? multipleMediasBuilder; - GalleryAlbum? selectedAlbum; - List _galleryAlbums = []; - List get galleryAlbums => _galleryAlbums; + GalleryMedia? _media; + GalleryMedia? get media => _media; + List get galleryAlbums => _media == null ? [] : _media!.albums; List _selectedFiles = []; List? _extraRecentMedia; List get selectedFiles => _selectedFiles; @@ -50,6 +65,26 @@ class PhoneGalleryController extends GetxController { List? get extraRecentMedia => _extraRecentMedia; bool _pickerMode = false; bool get pickerMode => _pickerMode; + late PageController pageController; + late PageController pickerPageController; + GalleryAlbum? selectedAlbum; + + void resetBottomSheetView() { + if (permissionGranted) { + isRecent = true; + if (selectedAlbum == null) { + pickerPageController.jumpToPage(0); + } else { + pageController.jumpToPage(0); + } + selectedAlbum = null; + update(); + } + } + + void updateConfig(Config? config) { + this.config = config ?? Config(); + } void updateSelectedFiles(List media) { _selectedFiles = media.map((e) => e).toList(); @@ -69,11 +104,26 @@ class PhoneGalleryController extends GetxController { update(); } - void changeAlbum(GalleryAlbum? album) { - selectedAlbum = album; + Future changeAlbum( + {required GalleryAlbum album, + required BuildContext context, + required PhoneGalleryController controller, + required bool singleMedia, + required bool isBottomSheet}) async { _selectedFiles.clear(); - update(); + selectedAlbum = album; updatePickerListener(); + await pageController.animateToPage(1, + duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + } + + Future backToPicker() async { + _selectedFiles.clear(); + _pickerMode = false; + pickerPageController = PageController(initialPage: 1); + await pageController.animateToPage(0, + duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + selectedAlbum = null; } void unselectMedia(MediaFile file) { @@ -99,10 +149,10 @@ class PhoneGalleryController extends GetxController { void switchPickerMode(bool value) { if (!value) { _selectedFiles.clear(); + updatePickerListener(); } _pickerMode = value; update(); - updatePickerListener(); } void updatePickerListener() { @@ -112,19 +162,33 @@ class PhoneGalleryController extends GetxController { } static Future promptPermissionSetting() async { + await PhoneGalleryController.requestStatus(Permission.storage); + if (Platform.isIOS) { + await PhoneGalleryController.requestStatus(Permission.photos); + } if (Platform.isIOS && - await Permission.storage.request().isGranted && - await Permission.photos.request().isGranted || - Platform.isAndroid && await Permission.storage.request().isGranted) { + await Permission.storage.isGranted && + await Permission.photos.isGranted || + Platform.isAndroid && await Permission.storage.isGranted) { return true; } return false; } + static Future requestStatus(Permission permission) async { + while (true) { + try { + await permission.request(); + break; + } catch (e) { + await Future.delayed(const Duration(milliseconds: 500), () {}); + } + } + } + Future initializeAlbums() async { - GalleryMedia? media = await PhoneGalleryController.collectGallery; - if (media != null) { - _galleryAlbums = media.albums; + _media = await PhoneGalleryController.collectGallery; + if (_media != null) { if (_extraRecentMedia != null) { GalleryAlbum? recentTmp = recent; if (recentTmp != null) { @@ -132,12 +196,24 @@ class PhoneGalleryController extends GetxController { recentTmp.files.any((file) => element.id == file.id)); } } + permissionGranted = true; + _isInitialized = true; + } else { + permissionGranted = false; + permissionListener(); } - - _isInitialized = true; update(); } + void permissionListener() { + Timer.periodic(const Duration(seconds: 1), (timer) async { + if (await Permission.storage.isGranted) { + initializeAlbums(); + timer.cancel(); + } + }); + } + static Future get collectGallery async { if (await promptPermissionSetting()) { List tempGalleryAlbums = []; @@ -205,8 +281,8 @@ class PhoneGalleryController extends GetxController { } GalleryAlbum? get recent { - return _galleryAlbums.isNotEmpty - ? _galleryAlbums.singleWhere((element) => element.album.name == "All") + return galleryAlbums.isNotEmpty + ? galleryAlbums.singleWhere((element) => element.album.name == "All") : null; } //GalleryAlbum? get recent { @@ -245,14 +321,13 @@ class PhoneGalleryController extends GetxController { } bool isSelectedMedia(MediaFile file) { - return _selectedFiles.any((element) => element.medium.id == file.medium.id); + return _selectedFiles.any((element) => element.id == file.id); } void disposeController() { - _galleryAlbums = []; + _media = null; _selectedFiles = []; _isInitialized = false; - selectedAlbum = null; Get.delete(); update(); } diff --git a/lib/functions/color.dart b/lib/functions/color.dart new file mode 100644 index 0000000..2cb41ae --- /dev/null +++ b/lib/functions/color.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +Color darken(Color color, [double amount = .03]) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(color); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + return hslDark.toColor(); +} + +Color lighten(Color color, [double amount = .07]) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(color); + final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + return hslLight.toColor(); +} diff --git a/lib/gallery_picker.dart b/lib/gallery_picker.dart index 91b285b..ef60f7a 100644 --- a/lib/gallery_picker.dart +++ b/lib/gallery_picker.dart @@ -15,13 +15,13 @@ export 'user_widgets/gallery_picker_builder.dart'; export 'user_widgets/photo_provider.dart'; export 'user_widgets/video_provider.dart'; export 'user_widgets/media_provider.dart'; -export 'views/bottom_sheet.dart'; +export 'views/picker_scaffold.dart'; export 'views/gallery_picker_view/gallery_picker_view.dart'; +import 'package:bottom_sheet_scaffold/bottom_sheet_scaffold.dart'; import 'package:flutter/material.dart'; import 'package:gallery_picker/models/gallery_media.dart'; import 'package:get/get.dart'; import '../../controller/gallery_controller.dart'; -import 'controller/bottom_sheet_controller.dart'; import 'controller/picker_listener.dart'; import 'models/config.dart'; import 'models/media_file.dart'; @@ -43,9 +43,6 @@ class GalleryPicker { if (GetInstance().isRegistered()) { Get.find().disposeController(); } - if (GetInstance().isRegistered()) { - Get.find().disposeController(); - } } static Future?> pickMedia( @@ -101,26 +98,35 @@ class GalleryPicker { } static Future openSheet() async { - if (GetInstance().isRegistered()) { - await Get.find().open(); - } + BottomSheetPanel.open(); } static Future closeSheet() async { - if (GetInstance().isRegistered()) { - await Get.find().close(); + BottomSheetPanel.close(); + if (GetInstance().isRegistered()) { + Get.find().resetBottomSheetView(); } } static bool get isSheetOpened { - if (GetInstance().isRegistered()) { - return Get.find().sheetController.isExpanded; - } else { - return false; - } + return BottomSheetPanel.isOpen; + } + + static bool get isSheetExpanded { + return BottomSheetPanel.isExpanded; + } + + static bool get isSheetCollapsed { + return BottomSheetPanel.isCollapsed; } static Future get collectGallery async { return await PhoneGalleryController.collectGallery; } + + static Future get initializeGallery async { + final controller = Get.put(PhoneGalleryController()); + await controller.initializeAlbums(); + return controller.media; + } } diff --git a/lib/models/config.dart b/lib/models/config.dart index ae55019..c752e93 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -4,6 +4,7 @@ import 'mode.dart'; class Config { late Widget selectIcon; + Widget? permissionDeniedPage; late Color backgroundColor, appbarColor, appbarIconColor, @@ -33,6 +34,7 @@ class Config { TextStyle? unselectedMenuStyle, TextStyle? textStyle, TextStyle? appbarTextStyle, + this.permissionDeniedPage, this.recents = "RECENTS", this.recent = "Recent", this.gallery = "GALLERY", diff --git a/lib/models/gallery_album.dart b/lib/models/gallery_album.dart index 297b327..839a857 100644 --- a/lib/models/gallery_album.dart +++ b/lib/models/gallery_album.dart @@ -10,7 +10,7 @@ import 'config.dart'; class GalleryAlbum { late Album album; - late List? thumbnail; + List? thumbnail; List dateCategories = []; late AlbumType type; int get count => @@ -49,8 +49,8 @@ class GalleryAlbum { Future initialize() async { List dateCategory = []; for (var medium in sortAlbumMediaDates((await album.listMedia()).items)) { - MediaFile mediaFile = MediaFile(medium: medium); - String name = getDateCategory(medium); + MediaFile mediaFile = MediaFile.medium(medium); + String name = getDateCategory(mediaFile); if (dateCategory.any((element) => element.name == name)) { dateCategory .singleWhere((element) => element.name == name) @@ -71,8 +71,9 @@ class GalleryAlbum { } DateTime? get lastDate { - if (dateCategories.isNotEmpty) { - return dateCategories.first.files.first.medium.lastDate; + if (dateCategories.isNotEmpty && + dateCategories.first.files.first.medium != null) { + return dateCategories.first.files.first.medium!.lastDate; } else { return null; } @@ -81,23 +82,22 @@ class GalleryAlbum { List get files => dateCategories.expand((element) => element.files).toList(); - String getDateCategory(Medium mediaFile) { + String getDateCategory(MediaFile media) { Config config = GetInstance().isRegistered() ? Get.find().config : Config(); - if (daysBetween(mediaFile.lastDate!) <= 3) { + DateTime? lastDate = media.lastModified; + lastDate = lastDate ?? DateTime.now(); + if (daysBetween(lastDate) <= 3) { return config.recent; - } else if (daysBetween(mediaFile.lastDate!) > 3 && - daysBetween(mediaFile.lastDate!) <= 7) { + } else if (daysBetween(lastDate) > 3 && daysBetween(lastDate) <= 7) { return config.lastWeek; - } else if (daysBetween(mediaFile.lastDate!) > 7 && - daysBetween(mediaFile.lastDate!) <= 31) { + } else if (daysBetween(lastDate) > 7 && daysBetween(lastDate) <= 31) { return config.lastMonth; - } else if (daysBetween(mediaFile.lastDate!) > 31 && - daysBetween(mediaFile.lastDate!) <= 365) { - return DateFormat.MMMM().format(mediaFile.lastDate!).toString(); + } else if (daysBetween(lastDate) > 31 && daysBetween(lastDate) <= 365) { + return DateFormat.MMMM().format(lastDate).toString(); } else { - return DateFormat.y().format(mediaFile.lastDate!).toString(); + return DateFormat.y().format(lastDate).toString(); } } @@ -113,7 +113,7 @@ class GalleryAlbum { } else if (b.lastDate == null) { return -1; } else { - return a.lastDate!.compareTo(b.lastDate!); + return b.lastDate!.compareTo(a.lastDate!); } }); return mediumList; @@ -125,19 +125,19 @@ class GalleryAlbum { for (var category in dateCategories) { category.files.sort((a, b) { - if (a.medium.lastDate == null) { + if (a.medium == null) { return 1; - } else if (b.medium.lastDate == null) { + } else if (b.medium == null) { return -1; } else { - return b.medium.lastDate!.compareTo(a.medium.lastDate!); + return b.medium!.lastDate!.compareTo(a.medium!.lastDate!); } }); } } void addFile(MediaFile file) { - String name = getDateCategory(file.medium); + String name = getDateCategory(file); if (dateCategories.any((element) => element.name == name)) { dateCategories .singleWhere((element) => element.name == name) diff --git a/lib/models/media_file.dart b/lib/models/media_file.dart index 2be8ae3..98e2c63 100644 --- a/lib/models/media_file.dart +++ b/lib/models/media_file.dart @@ -1,108 +1,66 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; import 'package:photo_gallery/photo_gallery.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; -import '../controller/gallery_controller.dart'; enum MediaType { image, video } class MediaFile { - late Medium medium; - late MediaType type; + Medium? _medium; + File? _file; Uint8List? thumbnail; - Uint8List? data; - late String id; - bool thumbnailFailed = false; - File? file; - bool _noMedium = false; - bool get isVideo => type == MediaType.video; - bool get isImage => type == MediaType.image; + late MediaType _type; + late String _id; + bool get isVideo => _type == MediaType.video; + bool get isImage => _type == MediaType.image; + Medium? get medium => _medium; + MediaType get type => _type; + String get id => _id; + File? get file => _file; + DateTime? get lastModified => + _medium != null ? _medium!.modifiedDate : _file!.lastModifiedSync(); - MediaFile({required this.medium}) { - type = medium.mediumType == MediumType.video + MediaFile.medium(Medium medium) { + _medium = medium; + _type = _medium!.mediumType == MediumType.video ? MediaType.video : MediaType.image; - id = medium.id; + _id = _medium!.id; } - MediaFile.file({required this.id, required this.file, required this.type}) { - _noMedium = true; - medium = Medium( - id: id, - mediumType: - type == MediaType.image ? MediumType.image : MediumType.video); + MediaFile.file( + {required String id, required File file, required MediaType type}) { + _file = file; + _id = id; + _type = type; } - Future getThumbnail() async { - if (thumbnail == null) { - try { - if (_noMedium) { - thumbnail = isVideo - ? await VideoThumbnail.thumbnailData( - video: file!.path, - imageFormat: ImageFormat.JPEG, - quality: 100, - ) - : await getData(); - } else { - thumbnail = - Uint8List.fromList(await medium.getThumbnail(highQuality: true)); - } - } catch (e) { - thumbnailFailed = true; - } + Future getThumbnail({bool highQuality = true}) async { + if (_medium == null) { + thumbnail = isVideo + ? (await VideoThumbnail.thumbnailData( + video: _file!.path, + imageFormat: ImageFormat.JPEG, + quality: highQuality ? 100 : 20, + ))! + : await getData(); + } else { + thumbnail = Uint8List.fromList( + await _medium!.getThumbnail(highQuality: highQuality)); } - return thumbnail; + return thumbnail!; } Future getFile() async { - if (file == null) { - file = await medium.getFile(); - return file!; + if (_medium != null) { + return await _medium!.getFile(); } else { - return file!; + return _file!; } } Future getData() async { - if (file == null) { - await getFile(); - } - data ??= await file!.readAsBytes(); - - return data!; - } - - void unselect({PhoneGalleryController? controller}) { - if (controller != null) { - controller.unselectMedia(this); - } else { - if (GetInstance().isRegistered()) { - Get.find().unselectMedia(this); - } - } - } - - void select({PhoneGalleryController? controller}) { - if (controller != null) { - controller.selectMedia(this); - } else { - if (GetInstance().isRegistered()) { - Get.find().selectMedia(this); - } - } - } - - bool? isSelected({PhoneGalleryController? controller}) { - if (controller != null) { - return controller.isSelectedMedia(this); - } else { - if (GetInstance().isRegistered()) { - return Get.find().isSelectedMedia(this); - } else { - return null; - } - } + _file ??= await getFile(); + return _file!.readAsBytesSync(); } } diff --git a/lib/user_widgets/photo_provider.dart b/lib/user_widgets/photo_provider.dart index 8bda4bf..d642f2d 100644 --- a/lib/user_widgets/photo_provider.dart +++ b/lib/user_widgets/photo_provider.dart @@ -1,59 +1,45 @@ import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; import '../models/media_file.dart'; -class PhotoProvider extends StatefulWidget { +class PhotoProvider extends StatelessWidget { final MediaFile media; final BoxFit fit; final double? width, height; + final Color? backgroundColor; + final Widget Function(BuildContext context)? onFailBuilder; const PhotoProvider({ super.key, required this.media, + this.onFailBuilder, this.fit = BoxFit.contain, + this.backgroundColor, this.width, this.height, }); - @override - State createState() => _PhotoProviderState(); -} - -class _PhotoProviderState extends State { - late MediaFile _media; - @override - void initState() { - _media = widget.media; - WidgetsBinding.instance.addPostFrameCallback((_) { - initMedia(); - }); - super.initState(); - } - - Future initMedia() async { - await _media.getData(); - if (mounted) { - setState(() {}); - } - } - @override Widget build(BuildContext context) { - if (_media != widget.media) { - _media = widget.media; - if (_media.data == null) { - initMedia(); - } - } - return _media.data == null - ? SizedBox( - width: widget.width, - height: widget.height, - ) - : Image.memory( - _media.data!, - width: widget.width, - height: widget.height, - fit: widget.fit, - ); + return FutureBuilder( + future: media.getData(), + builder: ((context, snapshot) { + return Container( + color: backgroundColor, + width: width, + height: height, + child: (snapshot.hasError && onFailBuilder != null) + ? onFailBuilder!(context) + : (snapshot.hasData) + ? FadeInImage( + fadeInDuration: const Duration(milliseconds: 200), + fit: BoxFit.cover, + placeholder: MemoryImage(kTransparentImage), + image: MemoryImage(snapshot.data!), + ) + : const SizedBox(), + ); + }), + ); } } diff --git a/lib/user_widgets/thumbnail_album.dart b/lib/user_widgets/thumbnail_album.dart index 26d110f..b4d0d71 100644 --- a/lib/user_widgets/thumbnail_album.dart +++ b/lib/user_widgets/thumbnail_album.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; import '/models/gallery_album.dart'; import '../models/mode.dart'; @@ -58,9 +59,11 @@ class ThumbnailAlbum extends StatelessWidget { color: failIconColor, )) else if (album.thumbnail != null) - Image.memory( - Uint8List.fromList(album.thumbnail!), + FadeInImage( + image: MemoryImage(Uint8List.fromList(album.thumbnail!)), + fadeInDuration: const Duration(milliseconds: 200), fit: BoxFit.cover, + placeholder: MemoryImage(kTransparentImage), ) else const SizedBox(), diff --git a/lib/user_widgets/thumbnail_media.dart b/lib/user_widgets/thumbnail_media.dart index ad221b4..a315aae 100644 --- a/lib/user_widgets/thumbnail_media.dart +++ b/lib/user_widgets/thumbnail_media.dart @@ -5,49 +5,83 @@ import 'package:transparent_image/transparent_image.dart'; class ThumbnailMedia extends StatelessWidget { final MediaFile media; final bool noIcon; + final double? width, height; + final Color? backgroundColor; + final BoxFit fit; + final bool highQuality; + final double radius, borderWidth; + final Color borderColor; final Widget Function(MediaFile media, BuildContext context)? onErrorBuilder; const ThumbnailMedia( {super.key, required this.media, + this.fit = BoxFit.cover, this.onErrorBuilder, + this.radius = 0, + this.highQuality = true, + this.borderColor = Colors.transparent, + this.borderWidth = 0, + this.width, + this.height, + this.backgroundColor, this.noIcon = false}); @override Widget build(BuildContext context) { return FutureBuilder( - future: media.thumbnail == null ? media.getThumbnail() : null, + future: media.thumbnail == null + ? media.getThumbnail(highQuality: highQuality) + : null, builder: (context, snapshot) { - return Stack( - fit: StackFit.passthrough, - children: [ - if (media.thumbnailFailed && onErrorBuilder == null) - Icon( - media.isImage - ? Icons.image_not_supported - : Icons.videocam_off_rounded, - color: Colors.grey, - ) - else if (media.thumbnailFailed && onErrorBuilder == null) - onErrorBuilder!(media, context) - else if (media.thumbnail != null) - FadeInImage( - fadeInDuration: const Duration(milliseconds: 200), - fit: BoxFit.cover, - placeholder: MemoryImage(kTransparentImage), - image: MemoryImage(media.thumbnail!), - ) - else - const SizedBox(), - if (media.thumbnail != null && !media.thumbnailFailed && !noIcon) - Positioned( - bottom: 10, - left: 10, - child: Icon( - media.isVideo ? Icons.video_camera_back : null, - color: Colors.white, - size: 20, - )), - ], + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: borderColor, width: borderWidth)), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Stack( + fit: StackFit.passthrough, + children: [ + if (snapshot.hasError && onErrorBuilder == null) + Center( + child: Icon( + media.isImage + ? Icons.image_not_supported + : Icons.videocam_off_rounded, + color: Colors.grey, + ), + ) + else if (snapshot.hasError && onErrorBuilder == null) + onErrorBuilder!(media, context) + else if (media.thumbnail != null) + FadeInImage( + width: width, + height: height, + fadeInDuration: const Duration(milliseconds: 200), + fit: fit, + placeholder: MemoryImage(kTransparentImage), + image: MemoryImage(media.thumbnail!), + ) + else + SizedBox( + width: width, + height: height, + ), + if (media.thumbnail != null && !noIcon) + Positioned( + bottom: 10, + left: 10, + child: Icon( + media.isVideo ? Icons.video_camera_back : null, + color: Colors.white, + size: 20, + )), + ], + ), + ), ); }); } diff --git a/lib/views/album_categories_view/album_categories_view.dart b/lib/views/album_categories_view/album_categories_view.dart index e2c52a9..f881d36 100644 --- a/lib/views/album_categories_view/album_categories_view.dart +++ b/lib/views/album_categories_view/album_categories_view.dart @@ -6,7 +6,14 @@ import '../../../controller/gallery_controller.dart'; class AlbumCategoriesView extends StatelessWidget { final PhoneGalleryController controller; final Config config; - AlbumCategoriesView(this.controller, {super.key}) + final bool isBottomSheet; + final bool singleMedia; + + AlbumCategoriesView( + {super.key, + required this.controller, + required this.isBottomSheet, + required this.singleMedia}) : config = controller.config; @override Widget build(BuildContext context) { @@ -19,7 +26,12 @@ class AlbumCategoriesView extends StatelessWidget { children: [ ...controller.galleryAlbums.map( (album) => GestureDetector( - onTap: () => controller.changeAlbum(album), + onTap: () => controller.changeAlbum( + album: album, + isBottomSheet: isBottomSheet, + controller: controller, + singleMedia: singleMedia, + context: context), child: Stack(fit: StackFit.passthrough, children: [ ThumbnailAlbum( album: album, diff --git a/lib/views/album_view/album_appbar.dart b/lib/views/album_view/album_appbar.dart index 6c54b48..b80f493 100644 --- a/lib/views/album_view/album_appbar.dart +++ b/lib/views/album_view/album_appbar.dart @@ -1,56 +1,51 @@ import 'package:flutter/material.dart'; import 'package:gallery_picker/models/gallery_album.dart'; -import '../../controller/bottom_sheet_controller.dart'; import '../../controller/gallery_controller.dart'; -import '../gallery_picker_view/tappable_appbar.dart'; class AlbumAppBar extends StatelessWidget with PreferredSizeWidget { final PhoneGalleryController controller; - final BottomSheetController? bottomSheetController; final GalleryAlbum album; + final bool isBottomSheet; const AlbumAppBar( {super.key, - required this.bottomSheetController, required this.album, - required this.controller}); + required this.controller, + required this.isBottomSheet}); @override Widget build(BuildContext context) { - return TappableAppbar( - controller: bottomSheetController, - child: AppBar( - elevation: 0, - foregroundColor: controller.config.appbarIconColor, - backgroundColor: controller.config.appbarColor, - leading: TextButton( - onPressed: () { - controller.changeAlbum(null); - }, - child: Icon( - Icons.arrow_back, - color: controller.config.appbarIconColor, - )), - title: getTitle(), - actions: [ - !controller.pickerMode - ? TextButton( - onPressed: () { - controller.switchPickerMode(true); - }, - child: Icon( - Icons.check_box_outlined, - color: controller.config.appbarIconColor, - )) - : const SizedBox() - ], - ), + return AppBar( + elevation: 0, + foregroundColor: controller.config.appbarIconColor, + backgroundColor: controller.config.appbarColor, + leading: TextButton( + onPressed: () async { + controller.backToPicker(); + }, + child: Icon( + Icons.arrow_back, + color: controller.config.appbarIconColor, + )), + title: getTitle(), + actions: [ + !controller.pickerMode + ? TextButton( + onPressed: () { + controller.switchPickerMode(true); + }, + child: Icon( + Icons.check_box_outlined, + color: controller.config.appbarIconColor, + )) + : const SizedBox() + ], ); } Widget getTitle() { if (!controller.pickerMode && controller.selectedFiles.isEmpty) { return Text( - album.name!, + album.name ?? "Unnamed Album", style: controller.config.appbarTextStyle, ); } else if (controller.pickerMode && controller.selectedFiles.isEmpty) { diff --git a/lib/views/album_view/album_medias_view.dart b/lib/views/album_view/album_medias_view.dart index e3d9d76..d021cbe 100644 --- a/lib/views/album_view/album_medias_view.dart +++ b/lib/views/album_view/album_medias_view.dart @@ -7,14 +7,15 @@ import 'selected_medias_view.dart'; class AlbumMediasView extends StatelessWidget { final PhoneGalleryController controller; final bool singleMedia; - final bool isCollapsedSheet; + final bool isBottomSheet; const AlbumMediasView( {super.key, required this.galleryAlbum, required this.controller, - required this.isCollapsedSheet, + required this.isBottomSheet, required this.singleMedia}); final GalleryAlbum galleryAlbum; + @override Widget build(BuildContext context) { return Stack( @@ -26,7 +27,7 @@ class AlbumMediasView extends StatelessWidget { category: category, controller: controller, singleMedia: singleMedia, - isCollapsedSheet: isCollapsedSheet, + isBottomSheet: isBottomSheet, ), ], ), @@ -35,6 +36,7 @@ class AlbumMediasView extends StatelessWidget { alignment: Alignment.bottomCenter, child: SelectedMediasView( controller: controller, + isBottomSheet: isBottomSheet, )) ], ); diff --git a/lib/views/album_view/album_page.dart b/lib/views/album_view/album_page.dart index 3bee5cd..f8ac9dd 100644 --- a/lib/views/album_view/album_page.dart +++ b/lib/views/album_view/album_page.dart @@ -2,38 +2,47 @@ import 'package:flutter/material.dart'; import 'package:gallery_picker/views/album_view/album_appbar.dart'; import '../../../controller/gallery_controller.dart'; import '../../../models/gallery_album.dart'; -import '../../controller/bottom_sheet_controller.dart'; -import '../../models/config.dart'; import 'album_medias_view.dart'; class AlbumPage extends StatelessWidget { final bool singleMedia; final PhoneGalleryController controller; - final BottomSheetController? bottomSheetController; - final GalleryAlbum album; - final bool isCollapsedSheet; + final GalleryAlbum? album; + final bool isBottomSheet; const AlbumPage( {super.key, required this.album, required this.controller, required this.singleMedia, - required this.isCollapsedSheet, - required this.bottomSheetController}); + required this.isBottomSheet}); @override Widget build(BuildContext context) { - Config config = controller.config; - return Scaffold( - backgroundColor: config.backgroundColor, - appBar: AlbumAppBar( - bottomSheetController: bottomSheetController, - album: album, - controller: controller), - body: AlbumMediasView( - galleryAlbum: album, - controller: controller, - isCollapsedSheet: isCollapsedSheet, - singleMedia: singleMedia, - ), - ); + return WillPopScope( + child: Scaffold( + backgroundColor: controller.config.backgroundColor, + appBar: album != null + ? AlbumAppBar( + album: album!, + controller: controller, + isBottomSheet: isBottomSheet, + ) + : null, + body: album != null + ? AlbumMediasView( + galleryAlbum: album!, + controller: controller, + isBottomSheet: isBottomSheet, + singleMedia: singleMedia, + ) + : Center( + child: Text( + "No Album Found", + style: controller.config.textStyle, + )), + ), + onWillPop: () async { + controller.backToPicker(); + return false; + }); } } diff --git a/lib/views/album_view/date_category_view.dart b/lib/views/album_view/date_category_view.dart index 8f8190b..2011a43 100644 --- a/lib/views/album_view/date_category_view.dart +++ b/lib/views/album_view/date_category_view.dart @@ -8,13 +8,13 @@ class DateCategoryWiew extends StatelessWidget { final PhoneGalleryController controller; final bool singleMedia; final DateCategory category; - final bool isCollapsedSheet; + final bool isBottomSheet; const DateCategoryWiew( {super.key, required this.category, required this.controller, - required this.isCollapsedSheet, + required this.isBottomSheet, required this.singleMedia}); int getRowCount() { @@ -45,14 +45,14 @@ class DateCategoryWiew extends StatelessWidget { size: MediaQuery.of(context).size.width, padding: EdgeInsets.zero, crossAxisCount: 4, - mainAxisSpacing: 1.0, - crossAxisSpacing: 1.0, + mainAxisSpacing: 3.0, + crossAxisSpacing: 3.0, children: [ ...category.files.map( (medium) => MediaView(medium, controller: controller, singleMedia: singleMedia, - isCollapsedSheet: isCollapsedSheet), + isBottomSheet: isBottomSheet), ), ], ), diff --git a/lib/views/album_view/media_view.dart b/lib/views/album_view/media_view.dart index 30b25a3..163fe73 100644 --- a/lib/views/album_view/media_view.dart +++ b/lib/views/album_view/media_view.dart @@ -1,6 +1,5 @@ +import 'package:bottom_sheet_scaffold/bottom_sheet_scaffold.dart'; import 'package:flutter/material.dart'; -import '../../controller/bottom_sheet_controller.dart'; -import 'package:get/get.dart'; import '../../../controller/gallery_controller.dart'; import '../../../models/media_file.dart'; import '../thumbnail_media_file.dart'; @@ -9,89 +8,69 @@ class MediaView extends StatelessWidget { final MediaFile file; final PhoneGalleryController controller; final bool singleMedia; - final bool isCollapsedSheet; + final bool isBottomSheet; const MediaView(this.file, {super.key, required this.controller, required this.singleMedia, - required this.isCollapsedSheet}); + required this.isBottomSheet}); @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, children: [ - GestureDetector( - onLongPress: () { - if (singleMedia) { - if (controller.heroBuilder != null) { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return controller.heroBuilder!(file.medium.id, file, context); - })); - } else { - controller.selectedFiles.add(file); - controller.onSelect(controller.selectedFiles); - controller.updatePickerListener(); - if (GetInstance().isRegistered()) { - Get.find().close(); + ThumbnailMediaFile( + onLongPress: () { + if (singleMedia) { + if (controller.heroBuilder != null) { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return controller.heroBuilder!(file.id, file, context); + })); } else { - Navigator.pop(context); - controller.disposeController(); + controller.selectedFiles.add(file); + controller.onSelect(controller.selectedFiles); + controller.updatePickerListener(); + if (isBottomSheet) { + BottomSheetPanel.close(); + } else { + Navigator.pop(context); + controller.disposeController(); + } } - } - } else { - file.select(controller: controller); - } - }, - onTap: () { - if (controller.pickerMode) { - file.isSelected(controller: controller)! - ? file.unselect(controller: controller) - : file.select(controller: controller); - } else { - if (controller.heroBuilder != null) { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return controller.heroBuilder!(file.medium.id, file, context); - })); } else { - controller.selectedFiles.add(file); - controller.onSelect(controller.selectedFiles); - controller.updatePickerListener(); - if (GetInstance().isRegistered()) { - Get.find().close(); - } else { - Navigator.pop(context); - controller.disposeController(); - } + controller.selectMedia(file); } - } - }, - child: ThumbnailMediaFile( - file: file, - failIconColor: controller.config.appbarIconColor, - controller: controller, - isCollapsedSheet: isCollapsedSheet), - ), - if (file.isSelected(controller: controller)!) - GestureDetector( - onTap: () { - file.isSelected(controller: controller)! - ? file.unselect(controller: controller) - : file.select(controller: controller); }, - child: Opacity( - opacity: 0.5, - child: Container( - color: Colors.black, - child: const Icon( - Icons.check, - color: Colors.white, - size: 45, - ), - ), - ), - ), + onTap: () { + if (controller.pickerMode) { + if (controller.isSelectedMedia(file)) { + controller.unselectMedia(file); + } else { + controller.selectMedia(file); + } + } else { + if (controller.heroBuilder != null) { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return controller.heroBuilder!(file.id, file, context); + })); + } else { + controller.selectedFiles.add(file); + controller.onSelect(controller.selectedFiles); + controller.updatePickerListener(); + if (isBottomSheet) { + BottomSheetPanel.close(); + } else { + Navigator.pop(context); + controller.disposeController(); + } + } + } + }, + file: file, + failIconColor: controller.config.appbarIconColor, + controller: controller), ], ); } diff --git a/lib/views/album_view/selected_media_thumbnail.dart b/lib/views/album_view/selected_media_thumbnail.dart new file mode 100644 index 0000000..65107a8 --- /dev/null +++ b/lib/views/album_view/selected_media_thumbnail.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; + +import '../../controller/gallery_controller.dart'; +import '../../functions/color.dart'; +import '../../models/mode.dart'; + +class SelectedMediaThumbnail extends StatelessWidget { + final Uint8List? data; + final double? width, height; + final BoxFit fit; + final PhoneGalleryController controller; + final Color failIconColor; + const SelectedMediaThumbnail({ + super.key, + required this.failIconColor, + required this.controller, + required this.data, + required this.width, + required this.height, + this.fit = BoxFit.cover, + }); + + Color adjustFailedBgColor() { + if (controller.config.mode == Mode.dark) { + return lighten( + controller.config.backgroundColor, + ); + } else { + return darken(controller.config.backgroundColor); + } + } + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: adjustFailedBgColor(), + borderRadius: BorderRadius.circular(5), + ), + child: data != null + ? FadeInImage( + width: width, + height: height, + fadeInDuration: const Duration(milliseconds: 200), + fit: fit, + placeholder: MemoryImage(kTransparentImage), + image: MemoryImage(data!), + ) + : Center( + child: Icon( + Icons.browser_not_supported, + size: 50, + color: failIconColor, + ), + ), + ); + } +} diff --git a/lib/views/album_view/selected_medias_view.dart b/lib/views/album_view/selected_medias_view.dart index c8a7b51..b335ba7 100644 --- a/lib/views/album_view/selected_medias_view.dart +++ b/lib/views/album_view/selected_medias_view.dart @@ -1,13 +1,15 @@ +import 'package:bottom_sheet_scaffold/bottom_sheet_scaffold.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../controller/bottom_sheet_controller.dart'; +import 'package:gallery_picker/views/thumbnail_media_file.dart'; import '../../controller/gallery_controller.dart'; import '../../models/config.dart'; class SelectedMediasView extends StatelessWidget { final PhoneGalleryController controller; final Config config; - SelectedMediasView({super.key, required this.controller}) + final bool isBottomSheet; + SelectedMediasView( + {super.key, required this.controller, required this.isBottomSheet}) : config = controller.config; @override @@ -27,37 +29,19 @@ class SelectedMediasView extends StatelessWidget { child: ListView( scrollDirection: Axis.horizontal, children: [ - for (var selectedMedia in controller.selectedFiles) + for (var mediaFile in controller.selectedFiles) Padding( padding: const EdgeInsets.only( top: 3.0, bottom: 3.0, right: 2), - child: !selectedMedia.thumbnailFailed - ? Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - image: DecorationImage( - fit: BoxFit.fill, - image: Image.memory( - selectedMedia.thumbnail!, - fit: BoxFit.fill, - ).image), - ), - child: const SizedBox( - width: 47, - height: 47, - ), - ) - : Container( - width: 47, - height: 47, - alignment: Alignment.center, - child: Icon( - selectedMedia.isImage - ? Icons.image_not_supported - : Icons.videocam_off_rounded, - color: Colors.grey, - ), - ), + child: ThumbnailMediaFile( + file: mediaFile, + width: 50, + height: 55, + radius: 5, + noIcon: true, + noSelectedIcon: true, + failIconColor: controller.config.appbarIconColor, + controller: controller), ), ], ), @@ -69,7 +53,7 @@ class SelectedMediasView extends StatelessWidget { Navigator.of(context).push( MaterialPageRoute(builder: (BuildContext context) { return controller.heroBuilder!( - controller.selectedFiles[0].medium.id, + controller.selectedFiles[0].id, controller.selectedFiles[0], context); })); @@ -81,8 +65,8 @@ class SelectedMediasView extends StatelessWidget { })); } else { controller.onSelect(controller.selectedFiles); - if (GetInstance().isRegistered()) { - Get.find().close(); + if (isBottomSheet) { + BottomSheetPanel.close(); } else { Navigator.pop(context); controller.disposeController(); diff --git a/lib/views/bottom_sheet.dart b/lib/views/bottom_sheet.dart deleted file mode 100644 index bfe16f4..0000000 --- a/lib/views/bottom_sheet.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:bottom_sheet_bar/bottom_sheet_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:gallery_picker/controller/gallery_controller.dart'; -import '/gallery_picker.dart'; -import 'package:get/get.dart'; -import '../controller/bottom_sheet_controller.dart'; - -class BottomSheetLayout extends StatefulWidget { - final Widget child; - final Config? config; - final List? initSelectedMedia; - final List? extraRecentMedia; - final bool singleMedia; - final Function(List selectedMedia) onSelect; - final Widget Function(String tag, MediaFile media, BuildContext context)? - heroBuilder; - final Widget Function(List media, BuildContext context)? - multipleMediaBuilder; - final bool startWithRecent; - BottomSheetLayout( - {super.key, - required this.child, - required this.onSelect, - this.config, - this.heroBuilder, - this.initSelectedMedia, - this.extraRecentMedia, - this.singleMedia = false, - this.multipleMediaBuilder, - this.startWithRecent = true}) { - if (GetInstance().isRegistered()) { - if (initSelectedMedia != null) { - Get.find() - .updateSelectedFiles(initSelectedMedia!); - } - if (extraRecentMedia != null) { - Get.find() - .updateExtraRecentMedia(extraRecentMedia!); - } - } - } - - @override - State createState() => _BottomSheetLayoutState(); -} - -class _BottomSheetLayoutState extends State { - BuildContext? collapsedContext; - bool viewCollapsedPicker = false; - BottomSheetBarController bottomSheetBarController = - BottomSheetBarController(); - late BottomSheetController controller; - - @override - void initState() { - controller = Get.put(BottomSheetController(bottomSheetBarController)); - super.initState(); - } - - check() async { - var sheetController = controller.sheetController; - if (collapsedContext != null) { - final RenderBox renderBox = - collapsedContext!.findRenderObject() as RenderBox; - if (renderBox.size.height > 200 && - !sheetController.isExpanded && - !viewCollapsedPicker) { - await Future.delayed(const Duration(milliseconds: 100)); - controller.appBarTapping = false; - setState(() { - viewCollapsedPicker = true; - }); - } else if ((renderBox.size.height <= 200 || sheetController.isExpanded)) { - if (viewCollapsedPicker) { - viewCollapsedPicker = false; - controller.appBarTapping = false; - await Future.delayed(const Duration(milliseconds: 10)); - setState(() {}); - } - } - } - } - - @override - void dispose() { - controller.disposeController(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { - return BottomSheetBar( - willPopScope: true, - height: 0, - color: Colors.transparent, - locked: - (controller.sheetController.isExpanded && !controller.appBarTapping) - ? true - : false, - controller: controller.sheetController, - expandedBuilder: (scrollController) { - check(); - return controller.sheetController.isExpanded - ? GalleryPickerView( - onSelect: widget.onSelect, - config: widget.config, - sheetController: bottomSheetBarController, - heroBuilder: widget.heroBuilder, - multipleMediaBuilder: widget.multipleMediaBuilder, - singleMedia: widget.singleMedia, - initSelectedMedia: widget.initSelectedMedia, - extraRecentMedia: widget.extraRecentMedia, - startWithRecent: widget.startWithRecent, - ) - : Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - color: Colors.transparent, - ); - }, - body: widget.child, - collapsed: GetBuilder( - builder: (controller) => ViewCollapsed( - picker: GalleryPickerView( - onSelect: widget.onSelect, - config: widget.config, - sheetController: bottomSheetBarController, - heroBuilder: widget.heroBuilder, - singleMedia: widget.singleMedia, - multipleMediaBuilder: widget.multipleMediaBuilder, - initSelectedMedia: widget.initSelectedMedia, - isCollapsedSheet: true, - extraRecentMedia: widget.extraRecentMedia, - startWithRecent: widget.startWithRecent, - ), - viewPicker: controller.isClosing ? false : viewCollapsedPicker, - onBuild: (context) { - collapsedContext = context; - }), - ), - ); - }); - } -} - -class ViewCollapsed extends StatelessWidget { - final GalleryPickerView picker; - final bool viewPicker; - final Function(BuildContext context) onBuild; - const ViewCollapsed({ - super.key, - required this.picker, - required this.onBuild, - required this.viewPicker, - }); - - @override - Widget build(BuildContext context) { - onBuild(context); - return Container( - height: 50, - color: Colors.transparent, - child: viewPicker ? picker : null, - ); - } -} diff --git a/lib/views/gallery_picker_view/gallery_picker_view.dart b/lib/views/gallery_picker_view/gallery_picker_view.dart index 275435c..951d7cb 100644 --- a/lib/views/gallery_picker_view/gallery_picker_view.dart +++ b/lib/views/gallery_picker_view/gallery_picker_view.dart @@ -1,7 +1,5 @@ -import 'package:bottom_sheet_bar/bottom_sheet_bar.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../controller/bottom_sheet_controller.dart'; import '../../controller/gallery_controller.dart'; import '../../models/config.dart'; import '../../models/gallery_album.dart'; @@ -9,6 +7,7 @@ import '../../models/media_file.dart'; import '../album_categories_view/album_categories_view.dart'; import '../album_view/album_page.dart'; import '../album_view/album_medias_view.dart'; +import 'permission_denied_view.dart'; import 'picker_appbar.dart'; import 'reload_gallery.dart'; @@ -20,11 +19,10 @@ class GalleryPickerView extends StatefulWidget { final Widget Function(List media, BuildContext context)? multipleMediaBuilder; final bool startWithRecent; - final BottomSheetBarController? sheetController; + final bool isBottomSheet; final List? initSelectedMedia; final List? extraRecentMedia; final bool singleMedia; - final bool isCollapsedSheet; const GalleryPickerView( {super.key, this.config, @@ -32,8 +30,7 @@ class GalleryPickerView extends StatefulWidget { this.initSelectedMedia, this.extraRecentMedia, this.singleMedia = false, - this.isCollapsedSheet = false, - this.sheetController, + this.isBottomSheet = false, this.heroBuilder, this.multipleMediaBuilder, this.startWithRecent = false}); @@ -44,43 +41,39 @@ class GalleryPickerView extends StatefulWidget { class _GalleryPickerState extends State { late PhoneGalleryController galleryController; - BottomSheetController? bottomSheetController; - late PageController _scrollController; bool noPhotoSeleceted = true; late Config config; @override void initState() { - _scrollController = - PageController(initialPage: widget.startWithRecent ? 0 : 1); if (GetInstance().isRegistered()) { galleryController = Get.find(); - config = galleryController.config; + if (galleryController.configurationCompleted) { + galleryController.updateConfig(widget.config); + } else { + galleryController.configuration(widget.config, + onSelect: widget.onSelect, + startWithRecent: widget.startWithRecent, + heroBuilder: widget.heroBuilder, + multipleMediasBuilder: widget.multipleMediaBuilder, + initSelectedMedias: widget.initSelectedMedia, + extraRecentMedia: widget.extraRecentMedia, + isRecent: widget.startWithRecent); + } } else { - galleryController = Get.put(PhoneGalleryController(widget.config, + galleryController = Get.put(PhoneGalleryController()); + galleryController.configuration(widget.config, onSelect: widget.onSelect, + startWithRecent: widget.startWithRecent, heroBuilder: widget.heroBuilder, multipleMediasBuilder: widget.multipleMediaBuilder, initSelectedMedias: widget.initSelectedMedia, extraRecentMedia: widget.extraRecentMedia, - isRecent: widget.startWithRecent)); - config = galleryController.config; - } - - if (widget.sheetController != null) { - if (GetInstance().isRegistered()) { - bottomSheetController = Get.find(); - } else { - bottomSheetController = - Get.put(BottomSheetController(widget.sheetController!)); - } - bottomSheetController!.galleryController = galleryController; + isRecent: widget.startWithRecent); } + config = galleryController.config; if (!galleryController.isInitialized) { galleryController.initializeAlbums(); } - if (galleryController.isRecent) { - _scrollController = PageController(initialPage: 0); - } super.initState(); } @@ -90,144 +83,153 @@ class _GalleryPickerState extends State { } GalleryAlbum? selectedAlbum; + @override Widget build(BuildContext context) { double width = MediaQuery.of(context).size.width; return GetBuilder(builder: (controller) { - if (controller.selectedAlbum == null && selectedAlbum != null) { - _scrollController = PageController(initialPage: 1); - } - selectedAlbum = controller.selectedAlbum; return GetInstance().isRegistered() - ? controller.selectedAlbum == null - ? Scaffold( - backgroundColor: config.backgroundColor, - appBar: PickerAppBar( - controller: controller, - bottomSheetController: bottomSheetController, - ), - body: Column( - children: [ - Container( - width: width, - height: 48, - color: config.appbarColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Container( - decoration: controller.isRecent - ? BoxDecoration( - border: Border( - bottom: BorderSide( - color: config.underlineColor, - width: 3.0, - ), - )) - : null, - height: 48, - width: width / 2, - child: TextButton( - onPressed: () { - _scrollController.animateTo(0, - duration: - const Duration(milliseconds: 50), - curve: Curves.easeIn); - setState(() { - controller.isRecent = true; - controller.switchPickerMode(false); - }); - }, - child: Text(config.recents, - style: controller.isRecent - ? config.selectedMenuStyle - : config.unselectedMenuStyle)), + ? controller.permissionGranted + ? PageView( + controller: controller.pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + Scaffold( + backgroundColor: config.backgroundColor, + appBar: PickerAppBar( + controller: controller, + isBottomSheet: widget.isBottomSheet, + ), + body: Column( + children: [ + Container( + width: width, + height: 48, + color: config.appbarColor, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Container( + decoration: controller.isRecent + ? BoxDecoration( + border: Border( + bottom: BorderSide( + color: config.underlineColor, + width: 3.0, + ), + )) + : null, + height: 48, + width: width / 2, + child: TextButton( + onPressed: () { + controller.pickerPageController + .animateToPage(0, + duration: const Duration( + milliseconds: 50), + curve: Curves.easeIn); + setState(() { + controller.isRecent = true; + controller.switchPickerMode(false); + }); + }, + child: Text(config.recents, + style: controller.isRecent + ? config.selectedMenuStyle + : config.unselectedMenuStyle)), + ), + Container( + decoration: !controller.isRecent + ? BoxDecoration( + border: Border( + bottom: BorderSide( + color: config.underlineColor, + width: 3.0, + ), + )) + : null, + height: 48, + width: width / 2, + child: TextButton( + onPressed: () { + controller.pickerPageController + .animateToPage(1, + duration: const Duration( + milliseconds: 50), + curve: Curves.easeIn); + controller.isRecent = false; + controller.switchPickerMode(false); + }, + child: Text( + config.gallery, + style: controller.isRecent + ? config.unselectedMenuStyle + : config.selectedMenuStyle, + )), + ) + ], ), - Container( - decoration: !controller.isRecent - ? BoxDecoration( - border: Border( - bottom: BorderSide( - color: config.underlineColor, - width: 3.0, - ), - )) - : null, - height: 48, - width: width / 2, - child: TextButton( - onPressed: () { - _scrollController.animateTo(width, - duration: - const Duration(milliseconds: 50), - curve: Curves.easeIn); + ), + Expanded( + child: PageView( + controller: controller.pickerPageController, + onPageChanged: (value) { + if (value == 0) { + controller.isRecent = true; + controller.switchPickerMode(false); + } else { controller.isRecent = false; controller.switchPickerMode(false); - }, - child: Text( - config.gallery, - style: controller.isRecent - ? config.unselectedMenuStyle - : config.selectedMenuStyle, - )), - ) - ], - ), + } + }, + scrollDirection: Axis.horizontal, + children: [ + controller.isInitialized && + controller.recent != null + ? AlbumMediasView( + galleryAlbum: controller.recent!, + controller: controller, + isBottomSheet: widget.isBottomSheet, + singleMedia: widget.singleMedia) + : const Center( + child: CircularProgressIndicator( + color: Colors.grey, + )), + AlbumCategoriesView( + controller: controller, + isBottomSheet: widget.isBottomSheet, + singleMedia: widget.singleMedia, + ) + ]), + ), + ], ), - Expanded( - child: PageView( - controller: _scrollController, - onPageChanged: (value) { - if (value == 0) { - controller.isRecent = true; - controller.switchPickerMode(false); - } else { - controller.isRecent = false; - controller.switchPickerMode(false); - } - }, - scrollDirection: Axis.horizontal, - children: [ - controller.isInitialized - ? AlbumMediasView( - galleryAlbum: controller.recent!, - controller: controller, - isCollapsedSheet: widget.isCollapsedSheet, - singleMedia: widget.singleMedia) - : const Center( - child: CircularProgressIndicator( - color: Colors.grey, - )), - AlbumCategoriesView(controller) - ]), - ), - ], - ), - ) - : AlbumPage( - controller: controller, - album: controller.selectedAlbum!, - singleMedia: widget.singleMedia, - isCollapsedSheet: widget.isCollapsedSheet, - bottomSheetController: bottomSheetController, + ), + AlbumPage( + album: controller.selectedAlbum, + controller: controller, + singleMedia: widget.singleMedia, + isBottomSheet: widget.isBottomSheet) + ], ) + : controller.config.permissionDeniedPage ?? + PermissionDeniedView( + config: controller.config, + ) : ReloadGallery( config, onpressed: () async { - if (widget.sheetController != null) { - bottomSheetController = - Get.put(BottomSheetController(widget.sheetController!)); - } - galleryController = Get.put(PhoneGalleryController(config, + galleryController = Get.put(PhoneGalleryController()); + galleryController.configuration(widget.config, onSelect: widget.onSelect, + startWithRecent: widget.startWithRecent, heroBuilder: widget.heroBuilder, + multipleMediasBuilder: widget.multipleMediaBuilder, initSelectedMedias: widget.initSelectedMedia, extraRecentMedia: widget.extraRecentMedia, - multipleMediasBuilder: widget.multipleMediaBuilder, - isRecent: widget.startWithRecent)); - await controller.initializeAlbums(); - if (bottomSheetController != null) { - bottomSheetController!.galleryController = galleryController; + isRecent: widget.startWithRecent); + if (!controller.isInitialized) { + await controller.initializeAlbums(); } setState(() {}); }, diff --git a/lib/views/gallery_picker_view/permission_denied_view.dart b/lib/views/gallery_picker_view/permission_denied_view.dart new file mode 100644 index 0000000..611eceb --- /dev/null +++ b/lib/views/gallery_picker_view/permission_denied_view.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:gallery_picker/gallery_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class PermissionDeniedView extends StatelessWidget { + final Config config; + const PermissionDeniedView({super.key, required this.config}); + + @override + Widget build(BuildContext context) { + return Container( + color: config.backgroundColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 50, + ), + Text( + "Please allow access to your photos", + style: TextStyle( + color: config.textStyle.color, + fontSize: 20, + fontWeight: FontWeight.w500), + ), + const SizedBox( + height: 10, + ), + Text( + "This lets access your photos and videos from your library.", + style: TextStyle( + color: config.textStyle.color, + ), + ), + const SizedBox( + height: 10, + ), + TextButton( + onPressed: () async { + await openAppSettings(); + }, + child: const Text("Enable library access"), + ) + ], + ), + ); + } +} diff --git a/lib/views/gallery_picker_view/picker_appbar.dart b/lib/views/gallery_picker_view/picker_appbar.dart index c821ad4..abc32c7 100644 --- a/lib/views/gallery_picker_view/picker_appbar.dart +++ b/lib/views/gallery_picker_view/picker_appbar.dart @@ -1,52 +1,45 @@ +import 'package:bottom_sheet_scaffold/bottom_sheet_scaffold.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../controller/bottom_sheet_controller.dart'; import '../../controller/gallery_controller.dart'; -import 'tappable_appbar.dart'; class PickerAppBar extends StatelessWidget with PreferredSizeWidget { final PhoneGalleryController controller; - final BottomSheetController? bottomSheetController; + final bool isBottomSheet; const PickerAppBar( - {super.key, - required this.bottomSheetController, - required this.controller}); + {super.key, required this.isBottomSheet, required this.controller}); @override Widget build(BuildContext context) { - return TappableAppbar( - controller: bottomSheetController, - child: AppBar( - elevation: 0, - backgroundColor: controller.config.appbarColor, - leading: TextButton( - onPressed: () async { - if (GetInstance().isRegistered()) { - bottomSheetController!.close(); - } else { - Navigator.pop(context); - await Future.delayed(const Duration(milliseconds: 500)); - controller.disposeController(); - } - }, - child: Icon( - Icons.arrow_back, - color: controller.config.appbarIconColor, - )), - title: getTitle(), - actions: [ - !controller.pickerMode && controller.isRecent - ? TextButton( - onPressed: () { - controller.switchPickerMode(true); - }, - child: Icon( - Icons.check_box_outlined, - color: controller.config.appbarIconColor, - )) - : const SizedBox() - ], - ), + return AppBar( + elevation: 0, + backgroundColor: controller.config.appbarColor, + leading: TextButton( + onPressed: () async { + if (isBottomSheet) { + BottomSheetPanel.close(); + } else { + Navigator.pop(context); + await Future.delayed(const Duration(milliseconds: 500)); + controller.disposeController(); + } + }, + child: Icon( + Icons.arrow_back, + color: controller.config.appbarIconColor, + )), + title: getTitle(), + actions: [ + !controller.pickerMode && controller.isRecent + ? TextButton( + onPressed: () { + controller.switchPickerMode(true); + }, + child: Icon( + Icons.check_box_outlined, + color: controller.config.appbarIconColor, + )) + : const SizedBox() + ], ); } diff --git a/lib/views/gallery_picker_view/tappable_appbar.dart b/lib/views/gallery_picker_view/tappable_appbar.dart deleted file mode 100644 index c9ae64c..0000000 --- a/lib/views/gallery_picker_view/tappable_appbar.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../controller/bottom_sheet_controller.dart'; - -class TappableAppbar extends StatelessWidget { - final BottomSheetController? controller; - final Widget child; - const TappableAppbar( - {super.key, required this.controller, required this.child}); - - @override - Widget build(BuildContext context) { - return GetInstance().isRegistered() - ? GestureDetector( - onLongPressEnd: (a) { - if (GetInstance().isRegistered()) { - controller!.tapingStatus(false); - } - }, - onPanCancel: () {}, - onPanDown: (a) { - if (GetInstance().isRegistered()) { - controller!.tapingStatus(true); - } - }, - child: child, - ) - : child; - } -} diff --git a/lib/views/picker_scaffold.dart b/lib/views/picker_scaffold.dart new file mode 100644 index 0000000..b0e8ec1 --- /dev/null +++ b/lib/views/picker_scaffold.dart @@ -0,0 +1,136 @@ +import 'package:bottom_sheet_scaffold/bottom_sheet_scaffold.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:gallery_picker/controller/gallery_controller.dart'; +import '/gallery_picker.dart'; +import 'package:get/get.dart'; + +class PickerScaffold extends StatelessWidget { + PickerScaffold( + {super.key, + required this.onSelect, + this.body, + this.appBar, + this.floatingActionButton, + this.floatingActionButtonLocation, + this.floatingActionButtonAnimator, + this.persistentFooterButtons, + this.persistentFooterAlignment = AlignmentDirectional.centerEnd, + this.drawer, + this.onDrawerChanged, + this.endDrawer, + this.onEndDrawerChanged, + this.bottomNavigationBar, + this.backgroundColor, + this.resizeToAvoidBottomInset, + this.primary = true, + this.drawerDragStartBehavior = DragStartBehavior.start, + this.extendBody = false, + this.extendBodyBehindAppBar = false, + this.drawerScrimColor, + this.drawerEdgeDragWidth, + this.drawerEnableOpenDragGesture = true, + this.endDrawerEnableOpenDragGesture = true, + this.restorationId, + this.config, + this.heroBuilder, + this.initSelectedMedia, + this.extraRecentMedia, + this.singleMedia = false, + this.multipleMediaBuilder,}) { + if (GetInstance().isRegistered()) { + if (initSelectedMedia != null) { + Get.find() + .updateSelectedFiles(initSelectedMedia!); + } + if (extraRecentMedia != null) { + Get.find() + .updateExtraRecentMedia(extraRecentMedia!); + } + } + } + + final Widget? body; + final bool extendBody; + final bool extendBodyBehindAppBar; + final PreferredSizeWidget? appBar; + final Widget? floatingActionButton; + final FloatingActionButtonLocation? floatingActionButtonLocation; + final FloatingActionButtonAnimator? floatingActionButtonAnimator; + final List? persistentFooterButtons; + final AlignmentDirectional persistentFooterAlignment; + final Widget? drawer; + final DrawerCallback? onDrawerChanged; + final Widget? endDrawer; + final DrawerCallback? onEndDrawerChanged; + final Color? drawerScrimColor; + final Color? backgroundColor; + final Widget? bottomNavigationBar; + final bool? resizeToAvoidBottomInset; + final bool primary; + final DragStartBehavior drawerDragStartBehavior; + final double? drawerEdgeDragWidth; + final bool drawerEnableOpenDragGesture; + final bool endDrawerEnableOpenDragGesture; + final String? restorationId; + final Config? config; + final List? initSelectedMedia; + final List? extraRecentMedia; + final bool singleMedia; + final Function(List selectedMedia) onSelect; + final Widget Function(String tag, MediaFile media, BuildContext context)? + heroBuilder; + final Widget Function(List media, BuildContext context)? + multipleMediaBuilder; + @override + Widget build(BuildContext context) { + return BottomSheetScaffold( + extendBody: extendBody, + extendBodyBehindAppBar: extendBodyBehindAppBar, + appBar: appBar, + floatingActionButton: floatingActionButton, + floatingActionButtonAnimator: floatingActionButtonAnimator, + floatingActionButtonLocation: floatingActionButtonLocation, + persistentFooterAlignment: persistentFooterAlignment, + persistentFooterButtons: persistentFooterButtons, + drawer: drawer, + onDrawerChanged: onDrawerChanged, + endDrawer: endDrawer, + onEndDrawerChanged: onEndDrawerChanged, + drawerDragStartBehavior: drawerDragStartBehavior, + drawerEdgeDragWidth: drawerEdgeDragWidth, + drawerEnableOpenDragGesture: drawerEnableOpenDragGesture, + drawerScrimColor: drawerScrimColor, + endDrawerEnableOpenDragGesture: endDrawerEnableOpenDragGesture, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + restorationId: restorationId, + primary: primary, + backgroundColor: backgroundColor, + bottomNavigationBar: bottomNavigationBar, + body: body, + bottomSheet: DraggableBottomSheet( + draggableBody: true, + maxHeight: MediaQuery.of(context).size.height, + onHide: () { + if (GetInstance().isRegistered()) { + Get.find().resetBottomSheetView(); + } + }, + body: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: GalleryPickerView( + onSelect: onSelect, + config: config, + heroBuilder: heroBuilder, + multipleMediaBuilder: multipleMediaBuilder, + singleMedia: singleMedia, + isBottomSheet: true, + initSelectedMedia: initSelectedMedia, + extraRecentMedia: extraRecentMedia, + startWithRecent: true, + )), + ), + ); + } +} diff --git a/lib/views/thumbnail_media_file.dart b/lib/views/thumbnail_media_file.dart index 557707a..3528c1b 100644 --- a/lib/views/thumbnail_media_file.dart +++ b/lib/views/thumbnail_media_file.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../controller/gallery_controller.dart'; +import '../functions/color.dart'; import '../models/mode.dart'; import '/models/media_file.dart'; import 'package:transparent_image/transparent_image.dart'; @@ -8,12 +9,29 @@ class ThumbnailMediaFile extends StatelessWidget { final MediaFile file; final Color failIconColor; final PhoneGalleryController controller; - final bool isCollapsedSheet; + final BoxFit fit; + final double? width, height; + final double radius, borderWidth; + final Color borderColor; + final bool noIcon, noSelectedIcon; + final bool highQuality; + final Function()? onTap; + final Function()? onLongPress; const ThumbnailMediaFile( {super.key, + this.fit = BoxFit.cover, required this.file, + this.width, + this.onLongPress, + this.onTap, + this.height, + this.radius = 0, + this.noIcon = false, + this.noSelectedIcon = false, + this.highQuality = true, + this.borderColor = Colors.transparent, + this.borderWidth = 0, required this.failIconColor, - required this.isCollapsedSheet, required this.controller}); Color adjustFailedBgColor() { @@ -26,70 +44,99 @@ class ThumbnailMediaFile extends StatelessWidget { } } - Color darken(Color color, [double amount = .03]) { - assert(amount >= 0 && amount <= 1); - final hsl = HSLColor.fromColor(color); - final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); - return hslDark.toColor(); - } - - Color lighten(Color color, [double amount = .05]) { - assert(amount >= 0 && amount <= 1); - final hsl = HSLColor.fromColor(color); - final hslLight = - hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); - return hslLight.toColor(); - } - @override Widget build(BuildContext context) { return FutureBuilder( - future: file.thumbnail == null ? file.getThumbnail() : null, + future: file.thumbnail == null + ? file.getThumbnail(highQuality: highQuality) + : null, builder: (context, snapshot) { - return Stack( - fit: StackFit.passthrough, - children: [ - if (file.thumbnailFailed) - Container( - color: adjustFailedBgColor(), - child: Icon( - file.isImage - ? Icons.image_not_supported - : Icons.videocam_off_rounded, - size: 50, - color: failIconColor, - )) - else if (file.thumbnail != null && - !isCollapsedSheet && - controller.heroBuilder != null) - Hero( - tag: file.medium.id, - child: FadeInImage( - fadeInDuration: const Duration(milliseconds: 200), - fit: BoxFit.cover, - placeholder: MemoryImage(kTransparentImage), - image: MemoryImage(file.thumbnail!), - ), - ) - else if (file.thumbnail != null && controller.heroBuilder == null) - FadeInImage( - fadeInDuration: const Duration(milliseconds: 200), - fit: BoxFit.cover, - placeholder: MemoryImage(kTransparentImage), - image: MemoryImage(file.thumbnail!), - ) - else - const SizedBox(), - if (file.thumbnail != null && !file.thumbnailFailed) - Positioned( - bottom: 10, - left: 10, - child: Icon( - file.isVideo ? Icons.video_camera_back : null, - color: Colors.white, - size: 20, - )), - ], + return GestureDetector( + onTap: onTap != null + ? () { + onTap!(); + } + : null, + onLongPress: onLongPress != null + ? () { + onLongPress!(); + } + : null, + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: adjustFailedBgColor(), + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: borderColor, width: borderWidth)), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Stack( + fit: StackFit.passthrough, + children: [ + if (snapshot.hasError) + Center( + child: Icon( + file.isImage + ? Icons.image_not_supported + : Icons.videocam_off_rounded, + size: 50, + color: failIconColor, + ), + ) + else if (file.thumbnail != null && + controller.heroBuilder != null) + Hero( + tag: file.id, + child: FadeInImage( + width: width, + height: height, + fadeInDuration: const Duration(milliseconds: 200), + fit: fit, + placeholder: MemoryImage(kTransparentImage), + image: MemoryImage(file.thumbnail!), + ), + ) + else if (file.thumbnail != null && + controller.heroBuilder == null) + FadeInImage( + width: width, + height: height, + fadeInDuration: const Duration(milliseconds: 200), + fit: fit, + placeholder: MemoryImage(kTransparentImage), + image: MemoryImage(file.thumbnail!), + ) + else + SizedBox( + width: width, + height: height, + ), + if (!noIcon && file.thumbnail != null) + Positioned( + bottom: 10, + left: 10, + child: Icon( + file.isVideo ? Icons.video_camera_back : null, + color: Colors.white, + size: 20, + )), + if (!noSelectedIcon && controller.isSelectedMedia(file)) + Opacity( + opacity: 0.5, + child: Container( + color: Colors.black, + child: const Icon( + Icons.check, + color: Colors.white, + size: 45, + ), + ), + ), + ], + ), + ), + ), ); }); } diff --git a/pubspec.yaml b/pubspec.yaml index cb2935e..b94aa93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: gallery_picker description: Gallery Picker is a flutter package that will allow you to pick media file(s), manage and navigate inside your gallery with modern tools and views. -version: 0.2.3 +version: 0.3.0 homepage: https://github.com/FlutterWay/gallery_picker environment: @@ -18,7 +18,8 @@ dependencies: get: ^4.6.5 video_thumbnail: ^0.5.3 intl: ^0.18.0 - bottom_sheet_bar: ^2.3.8 + bottom_sheet_scaffold: ^0.1.1 + page_transition: ^2.0.9 dev_dependencies: flutter_test: sdk: flutter