Start building delete

This commit is contained in:
Vishnu Mohandas 2020-04-12 18:08:49 +05:30
parent cd81ad9baa
commit 8462ae2d77
7 changed files with 192 additions and 72 deletions

View file

@ -11,11 +11,13 @@ class DatabaseHelper {
static final table = 'photos';
static final columnUploadedFileId = 'uploaded_file_id';
static final columnLocalId = 'local_id';
static final columnLocalPath = 'local_path';
static final columnThumbnailPath = 'thumbnail_path';
static final columnPath = 'path';
static final columnHash = 'hash';
static final columnIsDeleted = 'is_deleted';
static final columnSyncTimestamp = 'sync_timestamp';
// make this a singleton class
@ -44,10 +46,12 @@ class DatabaseHelper {
await db.execute('''
CREATE TABLE $table (
$columnLocalId TEXT,
$columnUploadedFileId INTEGER NOT NULL,
$columnLocalPath TEXT NOT NULL,
$columnThumbnailPath TEXT NOT NULL,
$columnPath TEXT,
$columnHash TEXT NOT NULL,
$columnIsDeleted INTEGER DEFAULT 0,
$columnSyncTimestamp TEXT
)
''');
@ -75,26 +79,27 @@ class DatabaseHelper {
Future<List<Photo>> getAllPhotos() async {
Database db = await instance.database;
var results = await db.query(table);
var results = await db.query(table, where: '$columnIsDeleted = 0');
return _convertToPhotos(results);
}
Future<List<Photo>> getAllDeletedPhotos() async {
Database db = await instance.database;
var results = await db.query(table, where: '$columnIsDeleted = 1');
return _convertToPhotos(results);
}
Future<List<Photo>> getPhotosToBeUploaded() async {
Database db = await instance.database;
var results = await db.query(table, where: '$columnPath IS NULL');
var results = await db.query(table, where: '$columnUploadedFileId = -1');
return _convertToPhotos(results);
}
// We are assuming here that the hash column in the map is set. The other
// column values will be used to update the row.
Future<int> updatePathAndTimestamp(
String hash, String path, String timestamp) async {
Future<int> updatePhoto(Photo photo) async {
Database db = await instance.database;
var row = new Map<String, dynamic>();
row[columnPath] = path;
row[columnSyncTimestamp] = timestamp;
return await db
.update(table, row, where: '$columnHash = ?', whereArgs: [hash]);
return await db.update(table, _getRowForPhoto(photo),
where: '$columnHash = ? AND $columnPath = ?',
whereArgs: [photo.hash, photo.localPath]);
}
Future<Photo> getPhotoByPath(String path) async {
@ -102,7 +107,7 @@ class DatabaseHelper {
var rows =
await db.query(table, where: '$columnPath =?', whereArgs: [path]);
if (rows.length > 0) {
return Photo.fromRow(rows[0]);
return _getPhotofromRow(rows[0]);
} else {
throw ("No cached photo");
}
@ -115,10 +120,26 @@ class DatabaseHelper {
0;
}
Future<int> markPhotoAsDeleted(Photo photo) async {
Database db = await instance.database;
var values = new Map<String, dynamic>();
values[columnIsDeleted] = 1;
return db.update(table, values,
where: '$columnHash =? AND $columnLocalPath =?',
whereArgs: [photo.hash, photo.localPath]);
}
Future<int> deletePhoto(Photo photo) async {
Database db = await instance.database;
return db.delete(table,
where: '$columnHash =? AND $columnLocalPath =?',
whereArgs: [photo.hash, photo.localPath]);
}
List<Photo> _convertToPhotos(List<Map<String, dynamic>> results) {
var photos = List<Photo>();
for (var result in results) {
photos.add(Photo.fromRow(result));
photos.add(_getPhotofromRow(result));
}
return photos;
}
@ -126,6 +147,8 @@ class DatabaseHelper {
Map<String, dynamic> _getRowForPhoto(Photo photo) {
var row = new Map<String, dynamic>();
row[columnLocalId] = photo.localId;
row[columnUploadedFileId] =
photo.uploadedFileId == null ? -1 : photo.uploadedFileId;
row[columnLocalPath] = photo.localPath;
row[columnThumbnailPath] = photo.thumbnailPath;
row[columnPath] = photo.path;
@ -133,4 +156,18 @@ class DatabaseHelper {
row[columnSyncTimestamp] = photo.syncTimestamp;
return row;
}
Photo _getPhotofromRow(Map<String, dynamic> row) {
Photo photo = Photo();
photo.localId = row[columnLocalId];
photo.uploadedFileId = row[columnUploadedFileId];
photo.localPath = row[columnLocalPath];
photo.thumbnailPath = row[columnThumbnailPath];
photo.path = row[columnPath];
photo.hash = row[columnHash];
photo.syncTimestamp = row[columnSyncTimestamp] == null
? -1
: int.parse(row[columnSyncTimestamp]);
return photo;
}
}

View file

@ -4,6 +4,7 @@ import 'package:crypto/crypto.dart';
import 'package:photo_manager/photo_manager.dart';
class Photo {
int uploadedFileId;
String localId;
String path;
String localPath;
@ -14,24 +15,16 @@ class Photo {
Photo();
Photo.fromJson(Map<String, dynamic> json)
: path = json["path"],
: uploadedFileId = json["fileId"],
path = json["path"],
hash = json["hash"],
thumbnailPath = json["thumbnailPath"],
syncTimestamp = json["syncTimestamp"];
Photo.fromRow(Map<String, dynamic> row)
: localId = row["local_id"],
localPath = row["local_path"],
thumbnailPath = row["thumbnail_path"],
path = row["path"],
hash = row["hash"],
syncTimestamp = row["sync_timestamp"] == null
? -1
: int.parse(row["sync_timestamp"]);
static Future<Photo> fromAsset(AssetEntity asset) async {
Photo photo = Photo();
var file = (await asset.originFile);
photo.uploadedFileId = -1;
photo.localId = asset.id;
photo.localPath = file.path;
photo.hash = getHash(file);

View file

@ -29,7 +29,9 @@ class PhotoSyncManager {
Future<void> init() async {
_updateDatabase().then((_) {
_syncPhotos();
_syncPhotos().then((_) {
_deletePhotos();
});
});
}
@ -80,9 +82,8 @@ class PhotoSyncManager {
List<Photo> photosToBeUploaded =
await DatabaseHelper.instance.getPhotosToBeUploaded();
for (Photo photo in photosToBeUploaded) {
var uploadedPhoto = await _uploadFile(photo.localPath, photo.hash);
await DatabaseHelper.instance.updatePathAndTimestamp(photo.hash,
uploadedPhoto.path, uploadedPhoto.syncTimestamp.toString());
var uploadedPhoto = await _uploadFile(photo);
await DatabaseHelper.instance.updatePhoto(uploadedPhoto);
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp);
}
}
@ -93,10 +94,6 @@ class PhotoSyncManager {
var path = externalPath + "/photos/";
for (Photo photo in diff) {
if (await DatabaseHelper.instance.containsPhotoHash(photo.hash)) {
await DatabaseHelper.instance.updatePathAndTimestamp(
photo.hash, photo.path, photo.syncTimestamp.toString());
continue;
} else {
var localPath = path + basename(photo.path);
await _dio
.download(Constants.ENDPOINT + "/" + photo.path, localPath)
@ -126,18 +123,40 @@ class PhotoSyncManager {
}
}
Future<Photo> _uploadFile(String path, String hash) async {
Future<Photo> _uploadFile(Photo localPhoto) async {
var formData = FormData.fromMap({
"file": await MultipartFile.fromFile(path, filename: basename(path)),
"file": await MultipartFile.fromFile(localPhoto.localPath,
filename: basename(localPhoto.localPath)),
"user": Constants.USER,
});
var response = await _dio
.post(Constants.ENDPOINT + "/upload", data: formData)
.catchError(_onError);
_logger.i(response.toString());
var photo = Photo.fromJson(response.data);
photo.localPath = path;
return photo;
if (response != null) {
_logger.i(response.toString());
var photo = Photo.fromJson(response.data);
photo.localPath = localPhoto.localPath;
photo.localId = localPhoto.localId;
return photo;
} else {
return null;
}
}
Future<void> _deletePhotos() async {
DatabaseHelper.instance.getAllDeletedPhotos().then((deletedPhotos) {
for (Photo deletedPhoto in deletedPhotos) {
_deletePhotoOnServer(deletedPhoto)
.then((value) => DatabaseHelper.instance.deletePhoto(deletedPhoto));
}
});
}
Future<void> _deletePhotoOnServer(Photo photo) async {
return _dio.post(Constants.ENDPOINT + "/delete", queryParameters: {
"user": Constants.USER,
"fileID": photo.uploadedFileId
}).catchError((e) => _onError(e));
}
void _onError(error) {

View file

@ -1,9 +1,16 @@
import 'dart:io';
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/photo_sync_manager.dart';
import 'package:myapp/ui/image_widget.dart';
import 'package:provider/provider.dart';
import 'package:share_extend/share_extend.dart';
import 'package:toast/toast.dart';
import 'change_notifier_builder.dart';
@ -17,43 +24,27 @@ class Gallery extends StatefulWidget {
}
class _GalleryState extends State<Gallery> {
Logger _logger = Logger();
int _crossAxisCount = 4;
PhotoLoader get photoLoader => Provider.of<PhotoLoader>(context);
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
_logger.i("Build with _crossAxisCount: " + _crossAxisCount.toString());
return GestureDetector(
onScaleUpdate: (ScaleUpdateDetails details) {
_logger.i("Scale update: " + details.horizontalScale.toString());
setState(() {
if (details.horizontalScale < 1) {
_crossAxisCount = 8;
} else if (details.horizontalScale < 2) {
_crossAxisCount = 5;
} else if (details.horizontalScale < 4) {
_crossAxisCount = 4;
} else if (details.horizontalScale < 8) {
_crossAxisCount = 2;
} else {
_crossAxisCount = 1;
}
});
},
child: ChangeNotifierBuilder(
value: photoLoader,
builder: (_, __) {
return GridView.builder(
return ChangeNotifierBuilder(
value: photoLoader,
builder: (_, __) {
return DraggableScrollbar.semicircle(
labelTextBuilder: (double offset) => Text("Hello!"),
labelConstraints: BoxConstraints.tightFor(width: 80.0, height: 30.0),
controller: _scrollController,
child: GridView.builder(
itemBuilder: _buildItem,
itemCount: photoLoader.getPhotos().length,
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _crossAxisCount,
));
},
),
crossAxisCount: 4,
)),
);
},
);
}
@ -64,7 +55,7 @@ class _GalleryState extends State<Gallery> {
routeToDetailPage(photo, context);
},
onLongPress: () {
Toast.show(photo.localPath, context);
_showPopup(photo, context);
},
child: Padding(
padding: const EdgeInsets.all(2.0),
@ -73,6 +64,76 @@ class _GalleryState extends State<Gallery> {
);
}
void _showPopup(Photo photo, BuildContext context) {
final action = CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(
child: Text("Share"),
isDefaultAction: true,
onPressed: () {
ShareExtend.share(photo.localPath, "image");
Navigator.pop(context);
},
),
CupertinoActionSheetAction(
child: Text("Delete"),
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
_showDeletePopup(photo, context);
},
)
],
cancelButton: CupertinoActionSheetAction(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context);
},
),
);
showCupertinoModalPopup(context: context, builder: (_) => action);
}
void _showDeletePopup(Photo photo, BuildContext context) {
final action = CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(
child: Text("Delete on device"),
isDestructiveAction: true,
onPressed: () {
DatabaseHelper.instance.deletePhoto(photo).then((_) {
File file = File(photo.localPath);
file.delete().then((_) {
photoLoader.reloadPhotos();
Navigator.pop(context);
});
});
},
),
CupertinoActionSheetAction(
child: Text("Delete everywhere [WiP]"),
isDestructiveAction: true,
onPressed: () {
DatabaseHelper.instance.markPhotoAsDeleted(photo).then((_) {
File file = File(photo.localPath);
file.delete().then((_) {
photoLoader.reloadPhotos();
Navigator.pop(context);
});
});
},
)
],
cancelButton: CupertinoActionSheetAction(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context);
},
),
);
showCupertinoModalPopup(context: context, builder: (_) => action);
}
void routeToDetailPage(Photo photo, BuildContext context) {
final page = DetailPage(photo);
Navigator.of(context).push(

View file

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:myapp/core/lru_map.dart';
import 'package:myapp/models/photo.dart';
import 'package:myapp/photo_loader.dart';
import 'package:myapp/ui/loading_widget.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:provider/provider.dart';
@ -34,7 +33,7 @@ class _ImageWidgetState extends State<ImageWidget> {
if (cachedImage != null) {
image = cachedImage;
} else {
if (widget.photo.localId.isNotEmpty) {
if (widget.photo.localId != null) {
image = FutureBuilder<Uint8List>(
future: AssetEntity(id: widget.photo.localId)
.thumbDataWithSize(size, size),
@ -47,7 +46,10 @@ class _ImageWidgetState extends State<ImageWidget> {
ImageLruCache.setData(path, size, image);
return image;
} else {
return loadWidget;
return Container(
alignment: Alignment.center,
color: Colors.grey[500],
);
}
},
);

View file

@ -71,6 +71,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.9"
draggable_scrollbar:
dependency: "direct main"
description:
name: draggable_scrollbar
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
flutter:
dependency: "direct main"
description: flutter

View file

@ -35,6 +35,7 @@ dependencies:
toast: ^0.1.5
image: ^2.1.4
share_extend: "^1.1.2"
draggable_scrollbar: ^0.0.4
dev_dependencies:
flutter_test: