Fix issues on iOS

This commit is contained in:
Vishnu Mohandas 2020-04-24 18:10:24 +05:30
parent 20f4f6324b
commit 4a892e2ce3
13 changed files with 181 additions and 179 deletions

View file

@ -14,11 +14,9 @@ class DatabaseHelper {
static final columnGeneratedId = '_id';
static final columnUploadedFileId = 'uploaded_file_id';
static final columnLocalId = 'local_id';
static final columnLocalPath = 'local_path';
static final columnRelativePath = 'relative_path';
static final columnThumbnailPath = 'thumbnail_path';
static final columnPath = 'path';
static final columnHash = 'hash';
static final columnTitle = 'title';
static final columnPathName = 'path_name';
static final columnRemotePath = 'remote_path';
static final columnIsDeleted = 'is_deleted';
static final columnCreateTimestamp = 'create_timestamp';
static final columnSyncTimestamp = 'sync_timestamp';
@ -51,11 +49,9 @@ class DatabaseHelper {
$columnGeneratedId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
$columnLocalId TEXT,
$columnUploadedFileId INTEGER NOT NULL,
$columnLocalPath TEXT NOT NULL,
$columnRelativePath TEXT NOT NULL,
$columnThumbnailPath TEXT NOT NULL,
$columnPath TEXT,
$columnHash TEXT NOT NULL,
$columnTitle TEXT NOT NULL,
$columnPathName TEXT NOT NULL,
$columnRemotePath TEXT,
$columnIsDeleted INTEGER DEFAULT 0,
$columnCreateTimestamp TEXT NOT NULL,
$columnSyncTimestamp TEXT
@ -103,16 +99,20 @@ class DatabaseHelper {
return _convertToPhotos(results);
}
Future<int> updatePhoto(Photo photo) async {
Future<int> updatePhoto(
int generatedId, String remotePath, int syncTimestamp) async {
Database db = await instance.database;
return await db.update(table, _getRowForPhoto(photo),
where: '$columnGeneratedId = ?', whereArgs: [photo.generatedId]);
var values = new Map<String, dynamic>();
values[columnRemotePath] = remotePath;
values[columnSyncTimestamp] = syncTimestamp;
return await db.update(table, values,
where: '$columnGeneratedId = ?', whereArgs: [generatedId]);
}
Future<Photo> getPhotoByPath(String path) async {
Database db = await instance.database;
var rows =
await db.query(table, where: '$columnPath =?', whereArgs: [path]);
await db.query(table, where: '$columnRemotePath =?', whereArgs: [path]);
if (rows.length > 0) {
return _getPhotofromRow(rows[0]);
} else {
@ -147,11 +147,9 @@ class DatabaseHelper {
row[columnLocalId] = photo.localId;
row[columnUploadedFileId] =
photo.uploadedFileId == null ? -1 : photo.uploadedFileId;
row[columnLocalPath] = photo.localPath;
row[columnRelativePath] = photo.relativePath;
row[columnThumbnailPath] = photo.thumbnailPath;
row[columnPath] = photo.path;
row[columnHash] = photo.hash;
row[columnTitle] = photo.title;
row[columnPathName] = photo.pathName;
row[columnRemotePath] = photo.remotePath;
row[columnCreateTimestamp] = photo.createTimestamp;
row[columnSyncTimestamp] = photo.syncTimestamp;
return row;
@ -162,11 +160,9 @@ class DatabaseHelper {
photo.generatedId = row[columnGeneratedId];
photo.localId = row[columnLocalId];
photo.uploadedFileId = row[columnUploadedFileId];
photo.localPath = row[columnLocalPath];
photo.relativePath = row[columnRelativePath];
photo.thumbnailPath = row[columnThumbnailPath];
photo.path = row[columnPath];
photo.hash = row[columnHash];
photo.title = row[columnTitle];
photo.pathName = row[columnPathName];
photo.remotePath = row[columnRemotePath];
photo.createTimestamp = int.parse(row[columnCreateTimestamp]);
photo.syncTimestamp = row[columnSyncTimestamp] == null
? -1

View file

@ -1,12 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:myapp/photo_loader.dart';
import 'package:myapp/photo_provider.dart';
import 'package:myapp/photo_sync_manager.dart';
import 'package:myapp/ui/home_widget.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:provider/provider.dart';
final provider = PhotoProvider();
@ -15,20 +12,7 @@ final logger = Logger();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
await provider.refreshGalleryList();
if (provider.list.length > 0) {
provider.list[0].assetList.then((assets) {
init(assets);
});
} else {
init(List<AssetEntity>());
}
}
Future<void> init(List<AssetEntity> assets) async {
var photoSyncManager = PhotoSyncManager(assets);
photoSyncManager.init();
provider.refreshGalleryList().then((_) => PhotoSyncManager(provider.list));
}
class MyApp extends StatelessWidget {

View file

@ -1,63 +1,81 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:logger/logger.dart';
import 'package:path/path.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:path/path.dart';
class Photo {
int generatedId;
int uploadedFileId;
String localId;
String path;
String localPath;
String relativePath;
String thumbnailPath;
String hash;
String title;
String pathName;
String remotePath;
int createTimestamp;
int syncTimestamp;
Photo();
Photo.fromJson(Map<String, dynamic> json)
: uploadedFileId = json["fileId"],
path = json["path"],
hash = json["hash"],
thumbnailPath = json["thumbnailPath"],
remotePath = json["path"],
createTimestamp = json["createTimestamp"],
syncTimestamp = json["syncTimestamp"];
static Future<Photo> fromAsset(AssetEntity asset) async {
static Future<Photo> fromAsset(
AssetPathEntity pathEntity, AssetEntity asset) async {
Photo photo = Photo();
photo.uploadedFileId = -1;
photo.localId = asset.id;
var file = await asset.originFile;
photo.localPath = file.path;
if (Platform.isAndroid) {
photo.relativePath = dirname((asset.relativePath.endsWith("/")
? asset.relativePath
: asset.relativePath + "/") +
asset.title);
} else {
photo.relativePath = dirname(photo.localPath);
}
photo.hash = getHash(file);
photo.thumbnailPath = photo.localPath;
photo.title = asset.title;
photo.pathName = pathEntity.name;
photo.createTimestamp = asset.createDateTime.microsecondsSinceEpoch;
return photo;
}
AssetEntity getAsset() {
return AssetEntity(id: localId);
Future<Uint8List> getBytes() {
final asset = AssetEntity(id: localId);
if (extension(title) == ".HEIC") {
return asset.originBytes.then((bytes) =>
FlutterImageCompress.compressWithList(bytes)
.then((result) => Uint8List.fromList(result)));
} else {
return asset.originBytes;
}
}
static String getHash(File file) {
return sha256.convert(file.readAsBytesSync()).toString();
Future<Uint8List> getOriginalBytes() {
return AssetEntity(id: localId).originBytes;
}
@override
String toString() {
return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, path: $path, localPath: $localPath, relativePath: $relativePath, thumbnailPath: $thumbnailPath, hash: $hash, createTimestamp: $createTimestamp, syncTimestamp: $syncTimestamp)';
return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, title: $title, pathName: $pathName, remotePath: $remotePath, createTimestamp: $createTimestamp, syncTimestamp: $syncTimestamp)';
}
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is Photo &&
o.generatedId == generatedId &&
o.uploadedFileId == uploadedFileId &&
o.localId == localId &&
o.title == title &&
o.pathName == pathName &&
o.remotePath == remotePath &&
o.createTimestamp == createTimestamp &&
o.syncTimestamp == syncTimestamp;
}
@override
int get hashCode {
return generatedId.hashCode ^
uploadedFileId.hashCode ^
localId.hashCode ^
title.hashCode ^
pathName.hashCode ^
remotePath.hashCode ^
createTimestamp.hashCode ^
syncTimestamp.hashCode;
}
}

View file

@ -88,8 +88,10 @@ class PhotoProvider extends ChangeNotifier {
if (!result) {
print("Did not get permission");
}
var galleryList =
await PhotoManager.getAssetPathList(type: RequestType.image);
final filterOptionGroup = FilterOptionGroup();
filterOptionGroup.setOption(AssetType.image, FilterOption(needTitle: true));
var galleryList = await PhotoManager.getAssetPathList(
type: RequestType.image, filterOption: filterOptionGroup);
galleryList.sort((s1, s2) {
return s2.assetCount.compareTo(s1.assetCount);

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:logger/logger.dart';
import 'package:myapp/db/db_helper.dart';
@ -16,51 +15,58 @@ import 'package:myapp/core/constants.dart' as Constants;
class PhotoSyncManager {
final _logger = Logger();
final _dio = Dio();
final List<AssetEntity> _assets;
static final _lastSyncTimestampKey = "last_sync_timestamp_0";
static final _lastDBUpdateTimestampKey = "last_db_update_timestamp";
PhotoSyncManager(this._assets) {
_logger.i("PhotoSyncManager init");
_assets.sort((first, second) => first.createDateTime.microsecondsSinceEpoch
.compareTo(second.createDateTime.microsecondsSinceEpoch));
PhotoSyncManager(List<AssetPathEntity> pathEntities) {
init(pathEntities);
}
Future<void> init() async {
_updateDatabase().then((_) {
_syncPhotos().then((_) {
_deletePhotos();
});
});
}
Future<bool> _updateDatabase() async {
Future<void> init(List<AssetPathEntity> pathEntities) async {
final prefs = await SharedPreferences.getInstance();
var lastDBUpdateTimestamp = prefs.getInt(_lastDBUpdateTimestampKey);
if (lastDBUpdateTimestamp == null) {
lastDBUpdateTimestamp = 0;
await _initializeDirectories();
}
var photos = List<Photo>();
var bufferLimit = 10;
final maxBufferLimit = 1000;
for (AssetEntity asset in _assets) {
if (asset.createDateTime.microsecondsSinceEpoch > lastDBUpdateTimestamp) {
try {
photos.add(await Photo.fromAsset(asset));
} catch (e) {
_logger.e(e);
}
if (photos.length > bufferLimit) {
await _insertPhotosToDB(
photos, prefs, asset.createDateTime.microsecondsSinceEpoch);
photos.clear();
bufferLimit = min(maxBufferLimit, bufferLimit * 2);
final photos = List<Photo>();
for (AssetPathEntity pathEntity in pathEntities) {
if (Platform.isIOS || pathEntity.name != "Recent") {
// "Recents" contain duplicate information on Android
var assetList = await pathEntity.assetList;
for (AssetEntity entity in assetList) {
if (entity.createDateTime.microsecondsSinceEpoch >
lastDBUpdateTimestamp) {
try {
photos.add(await Photo.fromAsset(pathEntity, entity));
} catch (e) {
_logger.e(e);
}
}
}
}
}
photos.sort((first, second) =>
first.createTimestamp.compareTo(second.createTimestamp));
_updateDatabase(photos, prefs, lastDBUpdateTimestamp).then((_) {
_syncPhotos().then((_) {
_deletePhotos();
});
});
}
Future<bool> _updateDatabase(final List<Photo> photos,
SharedPreferences prefs, int lastDBUpdateTimestamp) async {
var photosToBeAdded = List<Photo>();
for (Photo photo in photos) {
if (photo.createTimestamp > lastDBUpdateTimestamp) {
photosToBeAdded.add(photo);
}
}
return await _insertPhotosToDB(
photos, prefs, DateTime.now().microsecondsSinceEpoch);
photosToBeAdded, prefs, DateTime.now().microsecondsSinceEpoch);
}
_syncPhotos() async {
@ -89,7 +95,8 @@ class PhotoSyncManager {
if (uploadedPhoto == null) {
return;
}
await DatabaseHelper.instance.updatePhoto(uploadedPhoto);
await DatabaseHelper.instance.updatePhoto(photo.generatedId,
uploadedPhoto.remotePath, uploadedPhoto.syncTimestamp);
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp);
}
}
@ -98,12 +105,12 @@ class PhotoSyncManager {
var externalPath = (await getApplicationDocumentsDirectory()).path;
var path = externalPath + "/photos/";
for (Photo photo in diff) {
var localPath = path + basename(photo.path);
var localPath = path + basename(photo.remotePath);
await _dio
.download(Constants.ENDPOINT + "/" + photo.path, localPath)
.download(Constants.ENDPOINT + "/" + photo.remotePath, localPath)
.catchError(_onError);
photo.localPath = localPath;
photo.thumbnailPath = localPath;
// TODO: Save path
photo.pathName = localPath;
await DatabaseHelper.instance.insertPhoto(photo);
PhotoLoader.instance.reloadPhotos();
await prefs.setInt(_lastSyncTimestampKey, photo.syncTimestamp);
@ -128,8 +135,8 @@ class PhotoSyncManager {
Future<Photo> _uploadFile(Photo localPhoto) async {
var formData = FormData.fromMap({
"file": await MultipartFile.fromFile(localPhoto.localPath,
filename: basename(localPhoto.localPath)),
"file": MultipartFile.fromBytes(await localPhoto.getOriginalBytes()),
"filename": localPhoto.title,
"user": Constants.USER,
});
return _dio
@ -137,8 +144,6 @@ class PhotoSyncManager {
.then((response) {
_logger.i(response.toString());
var photo = Photo.fromJson(response.data);
photo.localPath = localPhoto.localPath;
photo.localId = localPhoto.localId;
return photo;
}).catchError(_onError);
}

View file

@ -42,7 +42,7 @@ class _AlbumListWidgetState extends State<AlbumListWidget> {
List<Album> _getAlbums(List<Photo> photos) {
final albumMap = new LinkedHashMap<String, List<Photo>>();
for (Photo photo in photos) {
final folder = path.basename(photo.relativePath);
final folder = path.basename(photo.pathName);
if (!albumMap.containsKey(folder)) {
albumMap[folder] = new List<Photo>();
}
@ -59,7 +59,7 @@ class _AlbumListWidgetState extends State<AlbumListWidget> {
return GestureDetector(
child: Column(
children: <Widget>[
ImageWidget(album.photos[0], size: 160),
ImageWidget(album.photos[0], size: 140),
Padding(padding: EdgeInsets.all(2)),
Expanded(
child: Text(

View file

@ -4,13 +4,10 @@ import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:myapp/core/lru_map.dart';
import 'package:myapp/models/photo.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_view/photo_view.dart';
import 'package:share_extend/share_extend.dart';
import 'extents_page_view.dart';
import 'loading_widget.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:myapp/utils/share_util.dart';
class DetailPage extends StatefulWidget {
final List<Photo> photos;
@ -27,11 +24,6 @@ class _DetailPageState extends State<DetailPage> {
int _selectedIndex = 0;
final _cachedImages = LRUMap<int, ZoomableImage>(5);
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
_selectedIndex = widget.selectedIndex;
@ -77,8 +69,8 @@ class _DetailPageState extends State<DetailPage> {
actions: <Widget>[
IconButton(
icon: Icon(Icons.share),
onPressed: () {
ShareExtend.share(widget.photos[_selectedIndex].localPath, "image");
onPressed: () async {
share(widget.photos[_selectedIndex]);
},
)
],
@ -99,26 +91,17 @@ class ZoomableImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
Logger().i("Building " + photo.generatedId.toString());
Logger().i("Building " + photo.toString());
if (ImageLruCache.getData(photo.generatedId) != null) {
return _buildPhotoView(ImageLruCache.getData(photo.generatedId));
}
var future;
if (path.extension(photo.localPath) == '.HEIC') {
Logger().i("Decoding HEIC");
future = photo.getAsset().originBytes.then((bytes) =>
FlutterImageCompress.compressWithList(bytes)
.then((result) => Uint8List.fromList(result)));
} else {
future = AssetEntity(id: photo.localId).originBytes;
}
return FutureBuilder<Uint8List>(
future: future,
future: photo.getBytes(),
builder: (_, snapshot) {
if (snapshot.hasData) {
return _buildPhotoView(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error);
return Text(snapshot.error.toString());
} else {
return loadWidget;
}

View file

@ -1,12 +1,11 @@
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:photo_manager/photo_manager.dart';
import 'package:provider/provider.dart';
import 'package:share_extend/share_extend.dart';
import 'package:myapp/utils/share_util.dart';
class GalleryAppBarWidget extends StatefulWidget
implements PreferredSizeWidget {
@ -65,11 +64,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
}
void _shareSelectedPhotos(BuildContext context) {
var photoPaths = List<String>();
for (Photo photo in widget.selectedPhotos) {
photoPaths.add(photo.localPath);
}
ShareExtend.shareMultiple(photoPaths, "image");
shareMultiple(widget.selectedPhotos.toList());
}
void _showDeletePhotosSheet(BuildContext context) {
@ -102,14 +97,14 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
Future _deleteSelectedPhotos(
BuildContext context, bool deleteEverywhere) async {
await PhotoManager.editor
.deleteWithIds(widget.selectedPhotos.map((p) => p.localId).toList());
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) {

View file

@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
@ -33,7 +32,7 @@ class _ImageWidgetState extends State<ImageWidget> {
Widget image;
if (cachedImageData != null) {
image = Image.memory(cachedImageData);
image = _buildImage(cachedImageData, size);
} else {
if (widget.photo.localId != null) {
image = FutureBuilder<Uint8List>(
@ -43,10 +42,7 @@ class _ImageWidgetState extends State<ImageWidget> {
if (snapshot.hasData) {
ImageLruCache.setData(
widget.photo.generatedId, size, snapshot.data);
Image image = Image.memory(snapshot.data,
width: size.toDouble(),
height: size.toDouble(),
fit: BoxFit.cover);
Image image = _buildImage(snapshot.data, size);
return image;
} else {
return loadingWidget;
@ -54,19 +50,16 @@ class _ImageWidgetState extends State<ImageWidget> {
},
);
} else {
image = Image.file(File(widget.photo.localPath),
width: size.toDouble(), height: size.toDouble(), fit: BoxFit.cover);
// TODO
return Text("Not Implemented");
}
}
return image;
}
@override
void didUpdateWidget(ImageWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.photo.generatedId != oldWidget.photo.generatedId) {
setState(() {});
}
Image _buildImage(Uint8List data, int size) {
return Image.memory(data,
width: size.toDouble(), height: size.toDouble(), fit: BoxFit.cover);
}
}

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:myapp/models/photo.dart';
import 'package:myapp/utils/gallery_items_filter.dart';
import 'package:path/path.dart';
@ -5,11 +7,14 @@ import 'package:path/path.dart';
class ImportantItemsFilter implements GalleryItemsFilter {
@override
bool shouldInclude(Photo photo) {
// TODO: Improve logic
final String folder = basename(photo.relativePath);
return folder == "Camera" ||
folder == "DCIM" ||
folder == "Download" ||
folder == "Screenshot";
if (Platform.isAndroid) {
final String folder = basename(photo.pathName);
return folder == "Camera" ||
folder == "DCIM" ||
folder == "Download" ||
folder == "Screenshot";
} else {
return true;
}
}
}

21
lib/utils/share_util.dart Normal file
View file

@ -0,0 +1,21 @@
import 'dart:typed_data';
import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:myapp/models/photo.dart';
import 'package:path/path.dart';
Future<void> share(Photo photo) async {
final bytes = await photo.getBytes();
final ext = extension(photo.title);
final shareExt =
ext == ".HEIC" ? "jpeg" : ext.substring(1, ext.length).toLowerCase();
return Share.file(photo.title, photo.title, bytes, "image/" + shareExt);
}
Future<void> shareMultiple(List<Photo> photos) async {
final shareContent = Map<String, Uint8List>();
for (Photo photo in photos) {
shareContent[photo.title] = await photo.getBytes();
}
return Share.files("images", shareContent, "*/*");
}

View file

@ -78,6 +78,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
esys_flutter_share:
dependency: "direct main"
description:
name: esys_flutter_share
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
flutter:
dependency: "direct main"
description: flutter
@ -233,13 +240,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
share_extend:
dependency: "direct main"
description:
name: share_extend
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
shared_preferences:
dependency: "direct main"
description:

View file

@ -34,7 +34,7 @@ dependencies:
crypto: ^2.1.3
toast: ^0.1.5
image: ^2.1.4
share_extend: "^1.1.2"
esys_flutter_share: ^1.0.2
draggable_scrollbar: ^0.0.4
photo_view: ^0.9.2
flutter_image_compress: ^0.6.5+1