Start building delete
This commit is contained in:
parent
cd81ad9baa
commit
8462ae2d77
7 changed files with 192 additions and 72 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue