Vishnu Mohandas 5 лет назад
Родитель
Сommit
d4d0ef78dc

+ 1 - 2
lib/db/db_helper.dart

@@ -1,6 +1,5 @@
 import 'dart:io';
 
-import 'package:logger/logger.dart';
 import 'package:myapp/models/photo.dart';
 import 'package:path/path.dart';
 import 'package:sqflite/sqflite.dart';
@@ -119,7 +118,7 @@ class DatabaseHelper {
     }
   }
 
-  Future<int> markPhotoAsDeleted(Photo photo) async {
+  Future<int> markPhotoForDeletion(Photo photo) async {
     Database db = await instance.database;
     var values = new Map<String, dynamic>();
     values[columnIsDeleted] = 1;

+ 0 - 1
lib/face_search_manager.dart

@@ -5,7 +5,6 @@ import 'package:myapp/db/db_helper.dart';
 
 import 'models/face.dart';
 import 'models/photo.dart';
-import 'models/search_result.dart';
 
 class FaceSearchManager {
   final _logger = Logger();

+ 8 - 0
lib/models/album.dart

@@ -0,0 +1,8 @@
+import 'package:myapp/models/photo.dart';
+
+class Album {
+  final String name;
+  final List<Photo> photos;
+
+  Album(this.name, this.photos);
+}

+ 0 - 2
lib/models/photo.dart

@@ -1,7 +1,6 @@
 import 'dart:io';
 
 import 'package:crypto/crypto.dart';
-import 'package:logger/logger.dart';
 import 'package:photo_manager/photo_manager.dart';
 
 class Photo {
@@ -26,7 +25,6 @@ class Photo {
         syncTimestamp = json["syncTimestamp"];
 
   static Future<Photo> fromAsset(AssetEntity asset) async {
-    Logger().i("From asset: " + asset.toString());
     Photo photo = Photo();
     var file = (await asset.originFile);
     photo.uploadedFileId = -1;

+ 86 - 0
lib/ui/album_list_widget.dart

@@ -0,0 +1,86 @@
+import 'dart:collection';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:myapp/models/album.dart';
+import 'package:myapp/models/photo.dart';
+import 'package:myapp/ui/album_widget.dart';
+import 'package:myapp/ui/image_widget.dart';
+
+class AlbumListWidget extends StatefulWidget {
+  final List<Photo> photos;
+
+  const AlbumListWidget(this.photos, {Key key}) : super(key: key);
+
+  @override
+  _AlbumListWidgetState createState() => _AlbumListWidgetState();
+}
+
+class _AlbumListWidgetState extends State<AlbumListWidget> {
+  @override
+  Widget build(BuildContext context) {
+    List<Album> albums = _getAlbums(widget.photos);
+
+    return Container(
+      margin: EdgeInsets.only(top: 24),
+      child: GridView.builder(
+        shrinkWrap: true,
+        padding: EdgeInsets.only(bottom: 12),
+        physics: ScrollPhysics(), // to disable GridView's scrolling
+        itemBuilder: (context, index) {
+          return _buildAlbum(context, albums[index]);
+        },
+        itemCount: albums.length,
+        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+          crossAxisCount: 2,
+        ),
+      ),
+    );
+  }
+
+  List<Album> _getAlbums(List<Photo> photos) {
+    final albumMap = new LinkedHashMap<String, List<Photo>>();
+    for (Photo photo in photos) {
+      final splitPath = photo.localPath.split("/");
+      final folder = splitPath[splitPath.length - 2];
+      if (!albumMap.containsKey(folder)) {
+        albumMap[folder] = new List<Photo>();
+      }
+      albumMap[folder].add(photo);
+    }
+    List<Album> albums = new List<Album>();
+    for (String albumName in albumMap.keys) {
+      albums.add(Album(albumName, albumMap[albumName]));
+    }
+    return albums;
+  }
+
+  Widget _buildAlbum(BuildContext context, Album album) {
+    return GestureDetector(
+      child: Column(
+        children: <Widget>[
+          ImageWidget(album.photos[0], size: 160),
+          Padding(padding: EdgeInsets.all(2)),
+          Expanded(
+            child: Text(
+              album.name,
+              style: TextStyle(
+                fontSize: 16,
+              ),
+            ),
+          ),
+        ],
+      ),
+      onTap: () {
+        final page = AlbumPage(album);
+        Navigator.of(context).push(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return page;
+            },
+          ),
+        );
+      },
+    );
+  }
+}

+ 52 - 0
lib/ui/album_widget.dart

@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:logger/logger.dart';
+import 'package:myapp/models/album.dart';
+import 'package:myapp/models/photo.dart';
+import 'package:myapp/ui/gallery.dart';
+import 'package:myapp/ui/gallery_app_bar_widget.dart';
+
+class AlbumPage extends StatefulWidget {
+  final Album album;
+
+  const AlbumPage(this.album, {Key key}) : super(key: key);
+
+  @override
+  _AlbumPageState createState() => _AlbumPageState();
+}
+
+class _AlbumPageState extends State<AlbumPage> {
+  Set<Photo> _selectedPhotos = Set<Photo>();
+
+  @override
+  Widget build(Object context) {
+    Logger().i("Building with " + widget.album.photos.length.toString());
+    return Scaffold(
+      appBar: GalleryAppBarWidget(
+        widget.album.name,
+        _selectedPhotos,
+        onSelectionClear: () {
+          setState(() {
+            _selectedPhotos.clear();
+          });
+        },
+        onPhotosDeleted: (deletedPhotos) {
+          setState(() {
+            for (Photo deletedPhoto in deletedPhotos) {
+              var index = widget.album.photos.indexOf(deletedPhoto);
+              Logger().i("Deleting " + index.toString());
+              widget.album.photos.removeAt(index);
+            }
+          });
+        },
+      ),
+      body: Gallery(
+        widget.album.photos,
+        photoSelectionChangeCallback: (Set<Photo> selectedPhotos) {
+          setState(() {
+            _selectedPhotos = selectedPhotos;
+          });
+        },
+      ),
+    );
+  }
+}

+ 0 - 2
lib/ui/detail_page.dart

@@ -25,11 +25,9 @@ class _DetailPageState extends State<DetailPage> {
   Widget build(BuildContext context) {
     _selectedIndex = widget.selectedIndex;
     var pageController = PageController(initialPage: _selectedIndex);
-
     return Scaffold(
       appBar: AppBar(
         actions: <Widget>[
-          // action button
           IconButton(
             icon: Icon(Icons.share),
             onPressed: () {

+ 5 - 8
lib/ui/gallery.dart

@@ -2,26 +2,23 @@ import 'dart:collection';
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:logger/logger.dart';
+import 'package:flutter/services.dart';
 import 'package:myapp/models/photo.dart';
 import 'package:myapp/photo_loader.dart';
+import 'package:myapp/ui/detail_page.dart';
 import 'package:myapp/ui/image_widget.dart';
 import 'package:myapp/utils/date_time_util.dart';
 import 'package:provider/provider.dart';
 
-import 'detail_page.dart';
-
 class Gallery extends StatefulWidget {
   final List<Photo> photos;
-  _GalleryState _state;
-  Function(Set<Photo>) photoSelectionChangeCallback;
+  final Function(Set<Photo>) photoSelectionChangeCallback;
 
   Gallery(this.photos, {this.photoSelectionChangeCallback});
 
   @override
   _GalleryState createState() {
-    _state = _GalleryState();
-    return _state;
+    return _GalleryState();
   }
 }
 
@@ -89,6 +86,7 @@ class _GalleryState extends State<Gallery> {
         }
       },
       onLongPress: () {
+        HapticFeedback.lightImpact();
         _selectPhoto(photo);
       },
       child: Container(
@@ -120,7 +118,6 @@ class _GalleryState extends State<Gallery> {
   }
 
   void routeToDetailPage(Photo photo, BuildContext context) {
-    Logger().i("Photo index: " + widget.photos.indexOf(photo).toString());
     final page = DetailPage(widget.photos, widget.photos.indexOf(photo));
     Navigator.of(context).push(
       MaterialPageRoute(

+ 129 - 0
lib/ui/gallery_app_bar_widget.dart

@@ -0,0 +1,129 @@
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:myapp/db/db_helper.dart';
+import 'package:myapp/models/photo.dart';
+import 'package:myapp/photo_loader.dart';
+import 'package:provider/provider.dart';
+import 'package:share_extend/share_extend.dart';
+
+class GalleryAppBarWidget extends StatefulWidget
+    implements PreferredSizeWidget {
+  final String title;
+  final Set<Photo> selectedPhotos;
+  final Function() onSelectionClear;
+  final Function(List<Photo>) onPhotosDeleted;
+
+  GalleryAppBarWidget(this.title, this.selectedPhotos,
+      {this.onSelectionClear, this.onPhotosDeleted});
+
+  @override
+  _GalleryAppBarWidgetState createState() => _GalleryAppBarWidgetState();
+
+  @override
+  Size get preferredSize => Size.fromHeight(60.0);
+}
+
+class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
+  PhotoLoader get photoLoader => Provider.of<PhotoLoader>(context);
+  @override
+  Widget build(BuildContext context) {
+    if (widget.selectedPhotos.isEmpty) {
+      return AppBar(title: Text(widget.title));
+    }
+
+    return AppBar(
+      leading: IconButton(
+        icon: Icon(Icons.close),
+        onPressed: () {
+          _clearSelectedPhotos();
+        },
+      ),
+      title: Text(widget.selectedPhotos.length.toString()),
+      actions: _getActions(context),
+    );
+  }
+
+  List<Widget> _getActions(BuildContext context) {
+    List<Widget> actions = List<Widget>();
+    if (widget.selectedPhotos.isNotEmpty) {
+      actions.add(IconButton(
+        icon: Icon(Icons.delete),
+        onPressed: () {
+          _showDeletePhotosSheet(context);
+        },
+      ));
+      actions.add(IconButton(
+        icon: Icon(Icons.share),
+        onPressed: () {
+          _shareSelectedPhotos(context);
+        },
+      ));
+    }
+    return actions;
+  }
+
+  void _shareSelectedPhotos(BuildContext context) {
+    var photoPaths = List<String>();
+    for (Photo photo in widget.selectedPhotos) {
+      photoPaths.add(photo.localPath);
+    }
+    ShareExtend.shareMultiple(photoPaths, "image");
+  }
+
+  void _showDeletePhotosSheet(BuildContext context) {
+    final action = CupertinoActionSheet(
+      actions: <Widget>[
+        CupertinoActionSheetAction(
+          child: Text("Delete on device"),
+          isDestructiveAction: true,
+          onPressed: () async {
+            await _deleteSelectedPhotos(context, false);
+          },
+        ),
+        CupertinoActionSheetAction(
+          child: Text("Delete everywhere [WiP]"),
+          isDestructiveAction: true,
+          onPressed: () async {
+            await _deleteSelectedPhotos(context, true);
+          },
+        )
+      ],
+      cancelButton: CupertinoActionSheetAction(
+        child: Text("Cancel"),
+        onPressed: () {
+          Navigator.of(context, rootNavigator: true).pop();
+        },
+      ),
+    );
+    showCupertinoModalPopup(context: context, builder: (_) => action);
+  }
+
+  Future _deleteSelectedPhotos(
+      BuildContext context, bool deleteEverywhere) async {
+    for (Photo photo in widget.selectedPhotos) {
+      deleteEverywhere
+          ? await DatabaseHelper.instance.markPhotoForDeletion(photo)
+          : await DatabaseHelper.instance.deletePhoto(photo);
+      File file = File(photo.localPath);
+      await file.delete();
+    }
+
+    Navigator.of(context, rootNavigator: true).pop();
+    photoLoader.reloadPhotos();
+    if (widget.onPhotosDeleted != null) {
+      widget.onPhotosDeleted(widget.selectedPhotos.toList());
+    }
+    _clearSelectedPhotos();
+  }
+
+  void _clearSelectedPhotos() {
+    setState(() {
+      widget.selectedPhotos.clear();
+    });
+    if (widget.onSelectionClear != null) {
+      widget.onSelectionClear();
+    }
+  }
+}

+ 50 - 65
lib/ui/gallery_container_widget.dart

@@ -1,27 +1,19 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
-import 'package:logger/logger.dart';
 import 'package:myapp/models/photo.dart';
+import 'package:myapp/photo_loader.dart';
 import 'package:myapp/ui/change_notifier_builder.dart';
+import 'package:myapp/ui/gallery.dart';
+import 'package:myapp/ui/loading_widget.dart';
 import 'package:myapp/ui/search_page.dart';
 import 'package:myapp/utils/important_items_filter.dart';
-import 'package:myapp/utils/gallery_items_filter.dart';
 import 'package:provider/provider.dart';
 
-import '../photo_loader.dart';
-import 'gallery.dart';
-import 'loading_widget.dart';
-
 // TODO: Remove redundant layer
 class GalleryContainer extends StatefulWidget {
-  final GalleryType type;
   final Function(Set<Photo>) photoSelectionChangeCallback;
 
-  static final importantItemsFilter = ImportantItemsFilter();
-  static final galleryItemsFilter = GalleryItemsFilter();
-
-  const GalleryContainer(
-    this.type, {
+  const GalleryContainer({
     Key key,
     this.photoSelectionChangeCallback,
   }) : super(key: key);
@@ -31,76 +23,69 @@ class GalleryContainer extends StatefulWidget {
 }
 
 class _GalleryContainerState extends State<GalleryContainer> {
+  static final importantItemsFilter = ImportantItemsFilter();
   PhotoLoader get photoLoader => Provider.of<PhotoLoader>(context);
 
   @override
   Widget build(BuildContext context) {
     return Column(
-      children: <Widget>[
-        Hero(
-            child: TextField(
-              readOnly: true,
-              onTap: () {
-                Navigator.of(context).push(
-                  MaterialPageRoute(
-                    builder: (BuildContext context) {
-                      return SearchPage();
-                    },
-                  ),
-                );
-              },
-              decoration: InputDecoration(
-                border: InputBorder.none,
-                hintText: 'Search "Paris"',
-                contentPadding: const EdgeInsets.all(12.0),
-              ),
-            ),
-            tag: "search"),
-        FutureBuilder<bool>(
-          future: photoLoader.loadPhotos(),
-          builder: (context, snapshot) {
-            if (snapshot.hasData) {
-              return ChangeNotifierBuilder(
+      children: <Widget>[_buildHero(context), _buildGallery()],
+    );
+  }
+
+  FutureBuilder<bool> _buildGallery() {
+    return FutureBuilder<bool>(
+      future: photoLoader.loadPhotos(),
+      builder: (context, snapshot) {
+        if (snapshot.hasData) {
+          return Flexible(
+              child: ChangeNotifierBuilder(
                   value: photoLoader,
                   builder: (_, __) {
-                    return Flexible(child: _getGallery(photoLoader.photos));
-                  });
-            } else if (snapshot.hasError) {
-              return Text("Error!");
-            } else {
-              return loadWidget;
-            }
-          },
-        )
-      ],
+                    return Gallery(
+                      getFilteredPhotos(photoLoader.photos),
+                      photoSelectionChangeCallback:
+                          widget.photoSelectionChangeCallback,
+                    );
+                  }));
+        } else if (snapshot.hasError) {
+          return Text("Error!");
+        } else {
+          return loadWidget;
+        }
+      },
     );
   }
 
-  Gallery _getGallery(List<Photo> photos) {
-    return widget.type == GalleryType.important_photos
-        ? Gallery(
-            getFilteredPhotos(photos, GalleryContainer.importantItemsFilter),
-            photoSelectionChangeCallback: widget.photoSelectionChangeCallback,
-          )
-        : Gallery(
-            getFilteredPhotos(photos, GalleryContainer.galleryItemsFilter),
-            photoSelectionChangeCallback: widget.photoSelectionChangeCallback,
-          );
+  Hero _buildHero(BuildContext context) {
+    return Hero(
+        child: TextField(
+          readOnly: true,
+          onTap: () {
+            Navigator.of(context).push(
+              MaterialPageRoute(
+                builder: (BuildContext context) {
+                  return SearchPage();
+                },
+              ),
+            );
+          },
+          decoration: InputDecoration(
+            border: InputBorder.none,
+            hintText: 'Search "Paris"',
+            contentPadding: const EdgeInsets.all(12.0),
+          ),
+        ),
+        tag: "search");
   }
 
-  List<Photo> getFilteredPhotos(
-      List<Photo> unfilteredPhotos, GalleryItemsFilter filter) {
+  List<Photo> getFilteredPhotos(List<Photo> unfilteredPhotos) {
     final List<Photo> filteredPhotos = List<Photo>();
     for (Photo photo in unfilteredPhotos) {
-      if (filter.shouldInclude(photo)) {
+      if (importantItemsFilter.shouldInclude(photo)) {
         filteredPhotos.add(photo);
       }
     }
     return filteredPhotos;
   }
 }
-
-enum GalleryType {
-  important_photos,
-  all_photos,
-}

+ 28 - 107
lib/ui/home_widget.dart

@@ -1,16 +1,13 @@
-import 'dart:io';
-
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
-import 'package:logger/logger.dart';
-import 'package:myapp/db/db_helper.dart';
 import 'package:myapp/models/photo.dart';
 import 'package:myapp/photo_loader.dart';
+import 'package:myapp/ui/album_list_widget.dart';
+import 'package:myapp/ui/change_notifier_builder.dart';
+import 'package:myapp/ui/gallery_app_bar_widget.dart';
+import 'package:myapp/ui/gallery_container_widget.dart';
 import 'package:provider/provider.dart';
-import 'package:share_extend/share_extend.dart';
-
-import 'gallery_container_widget.dart';
 
 class HomeWidget extends StatefulWidget {
   final String title;
@@ -32,18 +29,34 @@ class _HomeWidgetState extends State<HomeWidget> {
       title: widget.title,
       theme: ThemeData.dark(),
       home: Scaffold(
-        appBar: _buildAppBar(context),
-        bottomNavigationBar: _buildBottomNavigationBar(),
-        body: GalleryContainer(
-          _selectedNavBarItem == 0
-              ? GalleryType.important_photos
-              : GalleryType.all_photos,
-          photoSelectionChangeCallback: (Set<Photo> selectedPhotos) {
+        appBar: GalleryAppBarWidget(
+          widget.title,
+          _selectedPhotos,
+          onSelectionClear: () {
             setState(() {
-              _selectedPhotos = selectedPhotos;
+              _selectedPhotos.clear();
             });
           },
         ),
+        bottomNavigationBar: _buildBottomNavigationBar(),
+        body: IndexedStack(
+          children: <Widget>[
+            GalleryContainer(
+              photoSelectionChangeCallback: (Set<Photo> selectedPhotos) {
+                setState(() {
+                  _selectedPhotos = selectedPhotos;
+                });
+              },
+            ),
+            ChangeNotifierBuilder(
+              value: photoLoader,
+              builder: (_, __) {
+                return AlbumListWidget(photoLoader.photos);
+              },
+            )
+          ],
+          index: _selectedNavBarItem,
+        ),
       ),
     );
   }
@@ -69,96 +82,4 @@ class _HomeWidgetState extends State<HomeWidget> {
       },
     );
   }
-
-  Widget _buildAppBar(BuildContext context) {
-    if (_selectedPhotos.isEmpty) {
-      return AppBar(title: Text(widget.title));
-    }
-
-    return AppBar(
-      leading: IconButton(
-        icon: Icon(Icons.close),
-        onPressed: () {
-          setState(() {
-            _selectedPhotos.clear();
-          });
-        },
-      ),
-      title: Text(_selectedPhotos.length.toString()),
-      actions: _getActions(context),
-    );
-  }
-
-  List<Widget> _getActions(BuildContext context) {
-    List<Widget> actions = List<Widget>();
-    if (_selectedPhotos.isNotEmpty) {
-      actions.add(IconButton(
-        icon: Icon(Icons.delete),
-        onPressed: () {
-          _showDeletePhotosSheet(context);
-        },
-      ));
-      actions.add(IconButton(
-        icon: Icon(Icons.share),
-        onPressed: () {
-          _shareSelectedPhotos(context);
-        },
-      ));
-    }
-    return actions;
-  }
-
-  void _shareSelectedPhotos(BuildContext context) {
-    var photoPaths = List<String>();
-    for (Photo photo in _selectedPhotos) {
-      photoPaths.add(photo.localPath);
-    }
-    ShareExtend.shareMultiple(photoPaths, "image");
-  }
-
-  void _showDeletePhotosSheet(BuildContext context) {
-    final action = CupertinoActionSheet(
-      actions: <Widget>[
-        CupertinoActionSheetAction(
-          child: Text("Delete on device"),
-          isDestructiveAction: true,
-          onPressed: () async {
-            for (Photo photo in _selectedPhotos) {
-              await DatabaseHelper.instance.deletePhoto(photo);
-              File file = File(photo.localPath);
-              await file.delete();
-            }
-            photoLoader.reloadPhotos();
-            setState(() {
-              _selectedPhotos.clear();
-            });
-            Navigator.pop(context);
-          },
-        ),
-        CupertinoActionSheetAction(
-          child: Text("Delete everywhere [WiP]"),
-          isDestructiveAction: true,
-          onPressed: () async {
-            for (Photo photo in _selectedPhotos) {
-              await DatabaseHelper.instance.markPhotoAsDeleted(photo);
-              File file = File(photo.localPath);
-              await file.delete();
-            }
-            photoLoader.reloadPhotos();
-            setState(() {
-              _selectedPhotos.clear();
-            });
-            Navigator.pop(context);
-          },
-        )
-      ],
-      cancelButton: CupertinoActionSheetAction(
-        child: Text("Cancel"),
-        onPressed: () {
-          Navigator.pop(context);
-        },
-      ),
-    );
-    showCupertinoModalPopup(context: context, builder: (_) => action);
-  }
 }

+ 3 - 1
lib/ui/image_widget.dart

@@ -8,10 +8,12 @@ import 'package:photo_manager/photo_manager.dart';
 
 class ImageWidget extends StatefulWidget {
   final Photo photo;
+  final int size;
 
   const ImageWidget(
     this.photo, {
     Key key,
+    this.size,
   }) : super(key: key);
   @override
   _ImageWidgetState createState() => _ImageWidgetState();
@@ -25,7 +27,7 @@ class _ImageWidgetState extends State<ImageWidget> {
   @override
   Widget build(BuildContext context) {
     final path = widget.photo.localPath;
-    final size = 124;
+    final size = widget.size == null ? 124 : widget.size;
     final cachedImage = ImageLruCache.getData(path, size);
 
     Widget image;