Extend support for videos

This commit is contained in:
Vishnu Mohandas 2020-06-20 04:33:26 +05:30
parent 1e92a0ad08
commit a5aaf91460
35 changed files with 681 additions and 521 deletions

View file

@ -62,6 +62,10 @@ PODS:
- sqflite (0.0.1):
- Flutter
- FMDB (~> 2.7.2)
- video_player (0.0.1):
- Flutter
- video_player_web (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity (from `.symlinks/plugins/connectivity/ios`)
@ -84,6 +88,8 @@ DEPENDENCIES:
- shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`)
- shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- video_player (from `.symlinks/plugins/video_player/ios`)
- video_player_web (from `.symlinks/plugins/video_player_web/ios`)
SPEC REPOS:
trunk:
@ -135,6 +141,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_web/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
video_player:
:path: ".symlinks/plugins/video_player/ios"
video_player_web:
:path: ".symlinks/plugins/video_player_web/ios"
SPEC CHECKSUMS:
connectivity: 6e94255659cc86dcbef1d452ad3e0491bb1b3e75
@ -163,6 +173,8 @@ SPEC CHECKSUMS:
shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087
shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9
sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0
video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7
PODFILE CHECKSUM: dc81df99923cb3d9115f3b6c3d7e730abfec780b

View file

@ -1,29 +1,29 @@
import 'dart:io';
import 'dart:io' as dart;
import 'dart:typed_data';
import 'package:photos/core/cache/lru_map.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
class FileLruCache {
static LRUMap<int, File> _map = LRUMap(25);
static LRUMap<int, dart.File> _map = LRUMap(25);
static File get(Photo photo) {
return _map.get(photo.hashCode);
static dart.File get(File file) {
return _map.get(file.hashCode);
}
static void put(Photo photo, File imageData) {
_map.put(photo.hashCode, imageData);
static void put(File file, dart.File imageData) {
_map.put(file.hashCode, imageData);
}
}
class BytesLruCache {
static LRUMap<int, Uint8List> _map = LRUMap(25);
static Uint8List get(Photo photo) {
return _map.get(photo.hashCode);
static Uint8List get(File file) {
return _map.get(file.hashCode);
}
static void put(Photo photo, Uint8List imageData) {
_map.put(photo.hashCode, imageData);
static void put(File file, Uint8List imageData) {
_map.put(file.hashCode, imageData);
}
}

View file

@ -1,22 +1,22 @@
import 'dart:typed_data';
import 'package:photos/core/cache/lru_map.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
class ThumbnailLruCache {
static LRUMap<_ThumbnailCacheKey, Uint8List> _map = LRUMap(1000);
static Uint8List get(Photo photo, int size) {
static Uint8List get(File photo, int size) {
return _map.get(_ThumbnailCacheKey(photo, size));
}
static void put(Photo photo, int size, Uint8List imageData) {
static void put(File photo, int size, Uint8List imageData) {
_map.put(_ThumbnailCacheKey(photo, size), imageData);
}
}
class _ThumbnailCacheKey {
Photo photo;
File photo;
int size;
_ThumbnailCacheKey(this.photo, this.size);

View file

@ -1,20 +1,21 @@
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/location.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
class PhotoDB {
class FileDB {
// TODO: Use different tables within the same database
static final _databaseName = "ente.photos.db";
static final _databaseName = "ente.files.db";
static final _databaseVersion = 1;
static final Logger _logger = Logger("PhotoDB");
static final Logger _logger = Logger("FileDB");
static final table = 'photos';
static final table = 'files';
static final columnGeneratedId = '_id';
static final columnUploadedFileId = 'uploaded_file_id';
@ -23,6 +24,7 @@ class PhotoDB {
static final columnDeviceFolder = 'device_folder';
static final columnLatitude = 'latitude';
static final columnLongitude = 'longitude';
static final columnFileType = 'file_type';
static final columnRemoteFolderId = 'remote_folder_id';
static final columnRemotePath = 'remote_path';
static final columnThumbnailPath = 'thumbnail_path';
@ -31,8 +33,8 @@ class PhotoDB {
static final columnUpdateTimestamp = 'update_timestamp';
// make this a singleton class
PhotoDB._privateConstructor();
static final PhotoDB instance = PhotoDB._privateConstructor();
FileDB._privateConstructor();
static final FileDB instance = FileDB._privateConstructor();
// only have a single app-wide reference to the database
static Database _database;
@ -62,6 +64,7 @@ class PhotoDB {
$columnDeviceFolder TEXT NOT NULL,
$columnLatitude REAL,
$columnLongitude REAL,
$columnFileType INTEGER,
$columnRemoteFolderId INTEGER,
$columnRemotePath TEXT,
$columnThumbnailPath TEXT,
@ -72,37 +75,37 @@ class PhotoDB {
''');
}
Future<int> insertPhoto(Photo photo) async {
Future<int> insert(File file) async {
final db = await instance.database;
return await db.insert(table, _getRowForPhoto(photo));
return await db.insert(table, _getRowForFile(file));
}
Future<List<dynamic>> insertPhotos(List<Photo> photos) async {
Future<List<dynamic>> insertMultiple(List<File> files) async {
final db = await instance.database;
var batch = db.batch();
int batchCounter = 0;
for (Photo photo in photos) {
for (File file in files) {
if (batchCounter == 400) {
await batch.commit();
batch = db.batch();
}
batch.insert(table, _getRowForPhoto(photo));
batch.insert(table, _getRowForFile(file));
batchCounter++;
}
return await batch.commit();
}
Future<List<Photo>> getAllPhotos() async {
Future<List<File>> getAll() async {
final db = await instance.database;
final results = await db.query(
table,
where: '$columnLocalId IS NOT NULL AND $columnIsDeleted = 0',
orderBy: '$columnCreateTimestamp DESC',
);
return _convertToPhotos(results);
return _convertToFiles(results);
}
Future<List<Photo>> getAllPhotosInFolder(int folderId) async {
Future<List<File>> getAllInFolder(int folderId) async {
final db = await instance.database;
final results = await db.query(
table,
@ -110,30 +113,30 @@ class PhotoDB {
whereArgs: [folderId],
orderBy: '$columnCreateTimestamp DESC',
);
return _convertToPhotos(results);
return _convertToFiles(results);
}
Future<List<Photo>> getAllDeletedPhotos() async {
Future<List<File>> getAllDeleted() async {
final db = await instance.database;
final results = await db.query(
table,
where: '$columnIsDeleted = 1',
orderBy: '$columnCreateTimestamp DESC',
);
return _convertToPhotos(results);
return _convertToFiles(results);
}
Future<List<Photo>> getPhotosToBeUploaded() async {
Future<List<File>> getFilesToBeUploaded() async {
final db = await instance.database;
final results = await db.query(
table,
where: '$columnUploadedFileId IS NULL',
orderBy: '$columnCreateTimestamp DESC',
);
return _convertToPhotos(results);
return _convertToFiles(results);
}
Future<Photo> getMatchingPhoto(
Future<File> getMatchingFile(
String localId, String title, String deviceFolder, int createTimestamp,
{String alternateTitle}) async {
final db = await instance.database;
@ -150,13 +153,13 @@ class PhotoDB {
],
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
return _getFileFromRow(rows[0]);
} else {
throw ("No matching photo found");
throw ("No matching file found");
}
}
Future<Photo> getMatchingRemotePhoto(int uploadedFileId) async {
Future<File> getMatchingRemoteFile(int uploadedFileId) async {
final db = await instance.database;
final rows = await db.query(
table,
@ -164,13 +167,13 @@ class PhotoDB {
whereArgs: [uploadedFileId],
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
return _getFileFromRow(rows[0]);
} else {
throw ("No matching photo found");
throw ("No matching file found");
}
}
Future<int> updatePhoto(
Future<int> update(
int generatedId, int uploadedId, String remotePath, int updateTimestamp,
[String thumbnailPath]) async {
final db = await instance.database;
@ -187,22 +190,8 @@ class PhotoDB {
);
}
Future<Photo> getPhotoByPath(String path) async {
final db = await instance.database;
final rows = await db.query(
table,
where: '$columnRemotePath =?',
whereArgs: [path],
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
} else {
throw ("No cached photo");
}
}
// TODO: Remove deleted photos on remote
Future<int> markPhotoForDeletion(Photo photo) async {
// TODO: Remove deleted files on remote
Future<int> markForDeletion(File file) async {
final db = await instance.database;
var values = new Map<String, dynamic>();
values[columnIsDeleted] = 1;
@ -210,20 +199,20 @@ class PhotoDB {
table,
values,
where: '$columnGeneratedId =?',
whereArgs: [photo.generatedId],
whereArgs: [file.generatedId],
);
}
Future<int> deletePhoto(Photo photo) async {
Future<int> delete(File file) async {
final db = await instance.database;
return db.delete(
table,
where: '$columnGeneratedId =?',
whereArgs: [photo.generatedId],
whereArgs: [file.generatedId],
);
}
Future<int> deletePhotosInRemoteFolder(int folderId) async {
Future<int> deleteFilesInRemoteFolder(int folderId) async {
final db = await instance.database;
return db.delete(
table,
@ -247,7 +236,7 @@ class PhotoDB {
return result;
}
Future<Photo> getLatestPhotoInPath(String path) async {
Future<File> getLatestFileInPath(String path) async {
final db = await instance.database;
var rows = await db.query(
table,
@ -257,13 +246,13 @@ class PhotoDB {
limit: 1,
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
return _getFileFromRow(rows[0]);
} else {
throw ("No photo found in path");
throw ("No file found in path");
}
}
Future<Photo> getLatestPhotoInRemoteFolder(int folderId) async {
Future<File> getLatestFileInRemoteFolder(int folderId) async {
final db = await instance.database;
var rows = await db.query(
table,
@ -273,13 +262,13 @@ class PhotoDB {
limit: 1,
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
return _getFileFromRow(rows[0]);
} else {
throw ("No photo found in remote folder " + folderId.toString());
throw ("No file found in remote folder " + folderId.toString());
}
}
Future<Photo> getLastSyncedPhotoInRemoteFolder(int folderId) async {
Future<File> getLastSyncedFileInRemoteFolder(int folderId) async {
final db = await instance.database;
var rows = await db.query(
table,
@ -289,14 +278,13 @@ class PhotoDB {
limit: 1,
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
return _getFileFromRow(rows[0]);
} else {
throw ("No photo found in remote folder " + folderId.toString());
throw ("No file found in remote folder " + folderId.toString());
}
}
Future<Photo> getLatestPhotoAmongGeneratedIds(
List<String> generatedIds) async {
Future<File> getLatestFileAmongGeneratedIds(List<String> generatedIds) async {
final db = await instance.database;
var rows = await db.query(
table,
@ -305,55 +293,66 @@ class PhotoDB {
limit: 1,
);
if (rows.isNotEmpty) {
return _getPhotoFromRow(rows[0]);
return _getFileFromRow(rows[0]);
} else {
throw ("No photo found with ids " + generatedIds.join(", ").toString());
throw ("No file found with ids " + generatedIds.join(", ").toString());
}
}
List<Photo> _convertToPhotos(List<Map<String, dynamic>> results) {
var photos = List<Photo>();
List<File> _convertToFiles(List<Map<String, dynamic>> results) {
var files = List<File>();
for (var result in results) {
photos.add(_getPhotoFromRow(result));
files.add(_getFileFromRow(result));
}
return photos;
return files;
}
Map<String, dynamic> _getRowForPhoto(Photo photo) {
Map<String, dynamic> _getRowForFile(File file) {
var row = new Map<String, dynamic>();
row[columnLocalId] = photo.localId;
row[columnUploadedFileId] = photo.uploadedFileId;
row[columnTitle] = photo.title;
row[columnDeviceFolder] = photo.deviceFolder;
if (photo.location != null) {
row[columnLatitude] = photo.location.latitude;
row[columnLongitude] = photo.location.longitude;
row[columnLocalId] = file.localId;
row[columnUploadedFileId] = file.uploadedFileId;
row[columnTitle] = file.title;
row[columnDeviceFolder] = file.deviceFolder;
if (file.location != null) {
row[columnLatitude] = file.location.latitude;
row[columnLongitude] = file.location.longitude;
}
row[columnRemoteFolderId] = photo.remoteFolderId;
row[columnRemotePath] = photo.remotePath;
row[columnThumbnailPath] = photo.thumbnailPath;
row[columnCreateTimestamp] = photo.createTimestamp;
row[columnUpdateTimestamp] = photo.updateTimestamp;
switch (file.fileType) {
case FileType.image:
row[columnFileType] = 0;
break;
case FileType.video:
row[columnFileType] = 1;
break;
default:
row[columnFileType] = -1;
}
row[columnRemoteFolderId] = file.remoteFolderId;
row[columnRemotePath] = file.remotePath;
row[columnThumbnailPath] = file.previewURL;
row[columnCreateTimestamp] = file.createTimestamp;
row[columnUpdateTimestamp] = file.updateTimestamp;
return row;
}
Photo _getPhotoFromRow(Map<String, dynamic> row) {
Photo photo = Photo();
photo.generatedId = row[columnGeneratedId];
photo.localId = row[columnLocalId];
photo.uploadedFileId = row[columnUploadedFileId];
photo.title = row[columnTitle];
photo.deviceFolder = row[columnDeviceFolder];
File _getFileFromRow(Map<String, dynamic> row) {
File file = File();
file.generatedId = row[columnGeneratedId];
file.localId = row[columnLocalId];
file.uploadedFileId = row[columnUploadedFileId];
file.title = row[columnTitle];
file.deviceFolder = row[columnDeviceFolder];
if (row[columnLatitude] != null && row[columnLongitude] != null) {
photo.location = Location(row[columnLatitude], row[columnLongitude]);
file.location = Location(row[columnLatitude], row[columnLongitude]);
}
photo.remoteFolderId = row[columnRemoteFolderId];
photo.remotePath = row[columnRemotePath];
photo.thumbnailPath = row[columnThumbnailPath];
photo.createTimestamp = int.parse(row[columnCreateTimestamp]);
photo.updateTimestamp = row[columnUpdateTimestamp] == null
file.fileType = getFileType(row[columnFileType]);
file.remoteFolderId = row[columnRemoteFolderId];
file.remotePath = row[columnRemotePath];
file.previewURL = row[columnThumbnailPath];
file.createTimestamp = int.parse(row[columnCreateTimestamp]);
file.updateTimestamp = row[columnUpdateTimestamp] == null
? -1
: int.parse(row[columnUpdateTimestamp]);
return photo;
return file;
}
}

View file

@ -4,7 +4,7 @@ import 'package:photos/db/photo_db.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/face.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:photos/utils/file_name_util.dart';
class FaceSearchManager {
@ -28,7 +28,7 @@ class FaceSearchManager {
.catchError(_onError);
}
Future<List<Photo>> getFaceSearchResults(Face face) async {
Future<List<File>> getFaceSearchResults(Face face) async {
final result = await _dio
.get(
Configuration.instance.getHttpEndpoint() +
@ -42,24 +42,24 @@ class FaceSearchManager {
)
.then((response) {
return (response.data["result"] as List)
.map((p) => Photo.fromJson(p))
.map((p) => File.fromJson(p))
.toList();
}).catchError(_onError);
final photos = List<Photo>();
for (Photo photo in result) {
final files = List<File>();
for (File file in result) {
try {
photos.add(await PhotoDB.instance.getMatchingPhoto(photo.localId,
photo.title, photo.deviceFolder, photo.createTimestamp,
alternateTitle: getHEICFileNameForJPG(photo)));
files.add(await FileDB.instance.getMatchingFile(file.localId,
file.title, file.deviceFolder, file.createTimestamp,
alternateTitle: getHEICFileNameForJPG(file)));
} catch (e) {
// Not available locally
photos.add(photo);
files.add(file);
}
}
photos.sort((first, second) {
files.sort((first, second) {
return second.createTimestamp.compareTo(first.createTimestamp);
});
return photos;
return files;
}
void _onError(error) {

View file

@ -1,13 +1,13 @@
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FavoritePhotosRepository {
class FavoriteFilesRepository {
static final _favoritePhotoIdsKey = "favorite_photo_ids";
FavoritePhotosRepository._privateConstructor();
static FavoritePhotosRepository instance =
FavoritePhotosRepository._privateConstructor();
FavoriteFilesRepository._privateConstructor();
static FavoriteFilesRepository instance =
FavoriteFilesRepository._privateConstructor();
SharedPreferences _preferences;
@ -15,7 +15,7 @@ class FavoritePhotosRepository {
_preferences = await SharedPreferences.getInstance();
}
bool isLiked(Photo photo) {
bool isLiked(File photo) {
return getLiked().contains(photo.generatedId.toString());
}
@ -23,7 +23,7 @@ class FavoritePhotosRepository {
return getLiked().isNotEmpty;
}
Future<bool> setLiked(Photo photo, bool isLiked) {
Future<bool> setLiked(File photo, bool isLiked) {
final liked = getLiked();
if (isLiked) {
liked.add(photo.generatedId.toString());

33
lib/file_repository.dart Normal file
View file

@ -0,0 +1,33 @@
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/photo_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
class FileRepository {
final _logger = Logger("PhotoRepository");
final _files = List<File>();
FileRepository._privateConstructor();
static final FileRepository instance = FileRepository._privateConstructor();
List<File> get files {
return _files;
}
Future<bool> loadFiles() async {
FileDB db = FileDB.instance;
var files = await db.getAll();
_files.clear();
_files.addAll(files);
return true;
}
Future<void> reloadFiles() async {
_logger.info("Reloading...");
await loadFiles();
Bus.instance.fire(LocalPhotosUpdatedEvent());
}
}

View file

@ -8,7 +8,7 @@ import 'package:photos/db/photo_db.dart';
import 'package:photos/events/remote_sync_event.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/models/folder.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'core/event_bus.dart';
@ -39,7 +39,7 @@ class FolderSharingService {
for (final currentFolder in currentFolders) {
if (!folders.contains(currentFolder)) {
_logger.info("Folder deleted: " + currentFolder.toString());
await PhotoDB.instance.deletePhotosInRemoteFolder(currentFolder.id);
await FileDB.instance.deleteFilesInRemoteFolder(currentFolder.id);
await FolderDB.instance.deleteFolder(currentFolder);
}
}
@ -58,25 +58,25 @@ class FolderSharingService {
Future<void> syncDiff(Folder folder) async {
int lastSyncTimestamp = 0;
try {
Photo photo =
await PhotoDB.instance.getLastSyncedPhotoInRemoteFolder(folder.id);
lastSyncTimestamp = photo.updateTimestamp;
File file =
await FileDB.instance.getLastSyncedFileInRemoteFolder(folder.id);
lastSyncTimestamp = file.updateTimestamp;
} catch (e) {
// Folder has never been synced
}
var diff = await getDiff(folder.id, lastSyncTimestamp, _diffLimit);
for (Photo photo in diff) {
for (File file in diff) {
try {
var existingPhoto =
await PhotoDB.instance.getMatchingRemotePhoto(photo.uploadedFileId);
await PhotoDB.instance.updatePhoto(
await FileDB.instance.getMatchingRemoteFile(file.uploadedFileId);
await FileDB.instance.update(
existingPhoto.generatedId,
photo.uploadedFileId,
photo.remotePath,
photo.updateTimestamp,
photo.thumbnailPath);
file.uploadedFileId,
file.remotePath,
file.updateTimestamp,
file.previewURL);
} catch (e) {
await PhotoDB.instance.insertPhoto(photo);
await FileDB.instance.insert(file);
}
}
if (diff.length == _diffLimit) {
@ -84,7 +84,7 @@ class FolderSharingService {
}
}
Future<List<Photo>> getDiff(
Future<List<File>> getDiff(
int folderId, int sinceTimestamp, int limit) async {
Response response = await _dio.get(
Configuration.instance.getHttpEndpoint() +
@ -99,13 +99,13 @@ class FolderSharingService {
).catchError((e) => _logger.severe(e));
if (response != null) {
return (response.data["diff"] as List).map((p) {
Photo photo = new Photo.fromJson(p);
photo.localId = null;
photo.remoteFolderId = folderId;
return photo;
File file = new File.fromJson(p);
file.localId = null;
file.remoteFolderId = folderId;
return file;
}).toList();
} else {
return List<Photo>();
return List<File>();
}
}

View file

@ -27,7 +27,7 @@ void _main() async {
WidgetsFlutterBinding.ensureInitialized();
await Configuration.instance.init();
FavoritePhotosRepository.instance.init();
FavoriteFilesRepository.instance.init();
_sync();
final SentryClient sentry = new SentryClient(dsn: SENTRY_DSN);

View file

@ -1,10 +1,10 @@
import 'package:photos/models/filters/gallery_items_filter.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
class DeviceFolder {
final String name;
final Photo thumbnailPhoto;
final File thumbnail;
final GalleryItemsFilter filter;
DeviceFolder(this.name, this.thumbnailPhoto, this.filter);
DeviceFolder(this.name, this.thumbnail, this.filter);
}

View file

@ -6,9 +6,10 @@ import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:path/path.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/location.dart';
class Photo {
class File {
int generatedId;
int uploadedFileId;
String localId;
@ -16,43 +17,57 @@ class Photo {
String deviceFolder;
int remoteFolderId;
String remotePath;
String thumbnailPath;
String previewURL;
int createTimestamp;
int updateTimestamp;
Location location;
FileType fileType;
Photo();
Photo.fromJson(Map<String, dynamic> json)
: uploadedFileId = json["fileID"],
localId = json["deviceFileID"],
deviceFolder = json["deviceFolder"],
title = json["title"],
remotePath = json["path"],
thumbnailPath = json["previewURL"],
createTimestamp = json["createTimestamp"],
File();
File.fromJson(Map<String, dynamic> json) {
uploadedFileId = json["fileID"];
localId = json["deviceFileID"];
deviceFolder = json["deviceFolder"];
title = json["title"];
fileType = getFileType(json["fileType"]);
remotePath = json["path"];
previewURL = json["previewURL"];
createTimestamp = json["createTimestamp"];
updateTimestamp = json["updateTimestamp"];
}
static Future<Photo> fromAsset(
static Future<File> fromAsset(
AssetPathEntity pathEntity, AssetEntity asset) async {
Photo photo = Photo();
photo.localId = asset.id;
photo.title = asset.title;
photo.deviceFolder = pathEntity.name;
photo.location = Location(asset.latitude, asset.longitude);
photo.createTimestamp = asset.createDateTime.microsecondsSinceEpoch;
if (photo.createTimestamp == 0) {
File file = File();
file.localId = asset.id;
file.title = asset.title;
file.deviceFolder = pathEntity.name;
file.location = Location(asset.latitude, asset.longitude);
switch (asset.type) {
case AssetType.image:
file.fileType = FileType.image;
break;
case AssetType.video:
file.fileType = FileType.video;
break;
default:
file.fileType = FileType.other;
break;
}
file.createTimestamp = asset.createDateTime.microsecondsSinceEpoch;
if (file.createTimestamp == 0) {
try {
final parsedDateTime = DateTime.parse(
basenameWithoutExtension(photo.title)
basenameWithoutExtension(file.title)
.replaceAll("IMG_", "")
.replaceAll("DCIM_", "")
.replaceAll("_", " "));
photo.createTimestamp = parsedDateTime.microsecondsSinceEpoch;
file.createTimestamp = parsedDateTime.microsecondsSinceEpoch;
} catch (e) {
photo.createTimestamp = asset.modifiedDateTime.microsecondsSinceEpoch;
file.createTimestamp = asset.modifiedDateTime.microsecondsSinceEpoch;
}
}
return photo;
return file;
}
Future<AssetEntity> getAsset() {
@ -88,19 +103,22 @@ class Photo {
}
String getThumbnailUrl() {
return Configuration.instance.getHttpEndpoint() + "/" + thumbnailPath;
return Configuration.instance.getHttpEndpoint() + "/" + previewURL;
}
@override
String toString() {
return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, title: $title, deviceFolder: $deviceFolder, location: $location, remotePath: $remotePath, createTimestamp: $createTimestamp, updateTimestamp: $updateTimestamp)';
return '''File(generatedId: $generatedId, uploadedFileId: $uploadedFileId,
localId: $localId, title: $title, deviceFolder: $deviceFolder,
location: $location, remotePath: $remotePath, fileType: $fileType,
createTimestamp: $createTimestamp, updateTimestamp: $updateTimestamp)''';
}
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is Photo &&
return o is File &&
o.generatedId == generatedId &&
o.uploadedFileId == uploadedFileId &&
o.localId == localId;

16
lib/models/file_type.dart Normal file
View file

@ -0,0 +1,16 @@
enum FileType {
image,
video,
other,
}
FileType getFileType(int fileType) {
switch (fileType) {
case 0:
return FileType.image;
case 1:
return FileType.video;
default:
return FileType.other;
}
}

View file

@ -1,10 +1,10 @@
import 'package:photos/favorite_photos_repository.dart';
import 'package:photos/models/filters/gallery_items_filter.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
class FavoriteItemsFilter implements GalleryItemsFilter {
@override
bool shouldInclude(Photo photo) {
return FavoritePhotosRepository.instance.isLiked(photo);
bool shouldInclude(File file) {
return FavoriteFilesRepository.instance.isLiked(file);
}
}

View file

@ -1,5 +1,5 @@
import 'package:photos/models/filters/gallery_items_filter.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:path/path.dart' as path;
class FolderNameFilter implements GalleryItemsFilter {
@ -8,7 +8,7 @@ class FolderNameFilter implements GalleryItemsFilter {
FolderNameFilter(this.folderName);
@override
bool shouldInclude(Photo photo) {
return path.basename(photo.deviceFolder) == folderName;
bool shouldInclude(File file) {
return path.basename(file.deviceFolder) == folderName;
}
}

View file

@ -1,7 +1,7 @@
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
class GalleryItemsFilter {
bool shouldInclude(Photo photo) {
bool shouldInclude(File file) {
return true;
}
}

View file

@ -1,14 +1,14 @@
import 'dart:io';
import 'package:photos/models/filters/gallery_items_filter.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:path/path.dart';
class ImportantItemsFilter implements GalleryItemsFilter {
@override
bool shouldInclude(Photo photo) {
bool shouldInclude(File file) {
if (Platform.isAndroid) {
final String folder = basename(photo.deviceFolder);
final String folder = basename(file.deviceFolder);
return folder == "Camera" ||
folder == "DCIM" ||
folder == "Download" ||

View file

@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
class Folder {
final int id;
@ -9,7 +9,7 @@ class Folder {
final String deviceFolder;
final Set<String> sharedWith;
final int updateTimestamp;
Photo thumbnailPhoto;
File thumbnailPhoto;
Folder(
this.id,

View file

@ -1,33 +0,0 @@
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/photo_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/photo.dart';
class PhotoRepository {
final _logger = Logger("PhotoRepository");
final _photos = List<Photo>();
PhotoRepository._privateConstructor();
static final PhotoRepository instance = PhotoRepository._privateConstructor();
List<Photo> get photos {
return _photos;
}
Future<bool> loadPhotos() async {
PhotoDB db = PhotoDB.instance;
var photos = await db.getAllPhotos();
_photos.clear();
_photos.addAll(photos);
return true;
}
Future<void> reloadPhotos() async {
_logger.info("Reloading...");
await loadPhotos();
Bus.instance.fire(LocalPhotosUpdatedEvent());
}
}

View file

@ -7,13 +7,13 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/db/photo_db.dart';
import 'package:photos/events/photo_upload_event.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/file_repository.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/utils/file_name_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:dio/dio.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/events/remote_sync_event.dart';
@ -21,7 +21,7 @@ import 'package:photos/events/remote_sync_event.dart';
class PhotoSyncManager {
final _logger = Logger("PhotoSyncManager");
final _dio = Dio();
final _db = PhotoDB.instance;
final _db = FileDB.instance;
bool _isSyncInProgress = false;
Future<void> _existingSync;
@ -73,25 +73,25 @@ class PhotoSyncManager {
final pathEntities =
await _getGalleryList(lastDBUpdateTimestamp, syncStartTimestamp);
final photos = List<Photo>();
final files = List<File>();
AssetPathEntity recents;
for (AssetPathEntity pathEntity in pathEntities) {
if (pathEntity.name == "Recent" || pathEntity.name == "Recents") {
recents = pathEntity;
} else {
await _addToPhotos(pathEntity, lastDBUpdateTimestamp, photos);
await _addToPhotos(pathEntity, lastDBUpdateTimestamp, files);
}
}
if (recents != null) {
await _addToPhotos(recents, lastDBUpdateTimestamp, photos);
await _addToPhotos(recents, lastDBUpdateTimestamp, files);
}
if (photos.isNotEmpty) {
photos.sort((first, second) =>
if (files.isNotEmpty) {
files.sort((first, second) =>
first.createTimestamp.compareTo(second.createTimestamp));
await _updateDatabase(
photos, prefs, lastDBUpdateTimestamp, syncStartTimestamp);
await PhotoRepository.instance.reloadPhotos();
files, prefs, lastDBUpdateTimestamp, syncStartTimestamp);
await FileRepository.instance.reloadFiles();
}
await _syncWithRemote(prefs);
}
@ -104,13 +104,14 @@ class PhotoSyncManager {
}
final filterOptionGroup = FilterOptionGroup();
filterOptionGroup.setOption(AssetType.image, FilterOption(needTitle: true));
filterOptionGroup.setOption(AssetType.video, FilterOption(needTitle: true));
filterOptionGroup.dateTimeCond = DateTimeCond(
min: DateTime.fromMicrosecondsSinceEpoch(fromTimestamp),
max: DateTime.fromMicrosecondsSinceEpoch(toTimestamp),
);
var galleryList = await PhotoManager.getAssetPathList(
hasAll: true,
type: RequestType.image,
type: RequestType.common,
filterOption: filterOptionGroup,
);
@ -122,16 +123,16 @@ class PhotoSyncManager {
}
Future _addToPhotos(AssetPathEntity pathEntity, int lastDBUpdateTimestamp,
List<Photo> photos) async {
List<File> files) async {
final assetList = await pathEntity.assetList;
for (AssetEntity entity in assetList) {
if (max(entity.createDateTime.microsecondsSinceEpoch,
entity.modifiedDateTime.microsecondsSinceEpoch) >
lastDBUpdateTimestamp) {
try {
final photo = await Photo.fromAsset(pathEntity, entity);
if (!photos.contains(photo)) {
photos.add(photo);
final file = await File.fromAsset(pathEntity, entity);
if (!files.contains(file)) {
files.add(file);
}
} catch (e) {
_logger.severe(e);
@ -151,25 +152,22 @@ class PhotoSyncManager {
await _deletePhotosOnServer();
}
Future<bool> _updateDatabase(
final List<Photo> photos,
SharedPreferences prefs,
int lastDBUpdateTimestamp,
int syncStartTimestamp) async {
var photosToBeAdded = List<Photo>();
for (Photo photo in photos) {
if (photo.createTimestamp > lastDBUpdateTimestamp) {
photosToBeAdded.add(photo);
Future<bool> _updateDatabase(final List<File> files, SharedPreferences prefs,
int lastDBUpdateTimestamp, int syncStartTimestamp) async {
var filesToBeAdded = List<File>();
for (File file in files) {
if (file.createTimestamp > lastDBUpdateTimestamp) {
filesToBeAdded.add(file);
}
}
return await _insertPhotosToDB(photosToBeAdded, prefs, syncStartTimestamp);
return await _insertFilesToDB(filesToBeAdded, prefs, syncStartTimestamp);
}
Future<void> _downloadDiff(SharedPreferences prefs) async {
var diff = await _getDiff(_getLastSyncTimestamp(prefs), _diffLimit);
if (diff != null && diff.isNotEmpty) {
await _storeDiff(diff, prefs);
PhotoRepository.instance.reloadPhotos();
FileRepository.instance.reloadFiles();
if (diff.length == _diffLimit) {
return await _downloadDiff(prefs);
}
@ -185,15 +183,15 @@ class PhotoSyncManager {
}
Future<void> _uploadDiff(SharedPreferences prefs) async {
List<Photo> photosToBeUploaded = await _db.getPhotosToBeUploaded();
List<File> photosToBeUploaded = await _db.getFilesToBeUploaded();
for (int i = 0; i < photosToBeUploaded.length; i++) {
Photo photo = photosToBeUploaded[i];
_logger.info("Uploading " + photo.toString());
File file = photosToBeUploaded[i];
_logger.info("Uploading " + file.toString());
try {
var uploadedPhoto = await _uploadFile(photo);
await _db.updatePhoto(photo.generatedId, uploadedPhoto.uploadedFileId,
uploadedPhoto.remotePath, uploadedPhoto.updateTimestamp);
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.updateTimestamp);
var uploadedFile = await _uploadFile(file);
await _db.update(file.generatedId, uploadedFile.uploadedFileId,
uploadedFile.remotePath, uploadedFile.updateTimestamp);
prefs.setInt(_lastSyncTimestampKey, uploadedFile.updateTimestamp);
Bus.instance.fire(PhotoUploadEvent(
completed: i + 1, total: photosToBeUploaded.length));
@ -204,24 +202,22 @@ class PhotoSyncManager {
}
}
Future _storeDiff(List<Photo> diff, SharedPreferences prefs) async {
for (Photo photo in diff) {
Future _storeDiff(List<File> diff, SharedPreferences prefs) async {
for (File file in diff) {
try {
var existingPhoto = await _db.getMatchingPhoto(photo.localId,
photo.title, photo.deviceFolder, photo.createTimestamp,
alternateTitle: getHEICFileNameForJPG(photo));
await _db.updatePhoto(existingPhoto.generatedId, photo.uploadedFileId,
photo.remotePath, photo.updateTimestamp, photo.thumbnailPath);
var existingPhoto = await _db.getMatchingFile(
file.localId, file.title, file.deviceFolder, file.createTimestamp,
alternateTitle: getHEICFileNameForJPG(file));
await _db.update(existingPhoto.generatedId, file.uploadedFileId,
file.remotePath, file.updateTimestamp, file.previewURL);
} catch (e) {
await _db.insertPhoto(photo);
await _db.insert(file);
}
// _logger.info(
// "Setting update timestamp to " + photo.updateTimestamp.toString());
await prefs.setInt(_lastSyncTimestampKey, photo.updateTimestamp);
await prefs.setInt(_lastSyncTimestampKey, file.updateTimestamp);
}
}
Future<List<Photo>> _getDiff(int lastSyncTimestamp, int limit) async {
Future<List<File>> _getDiff(int lastSyncTimestamp, int limit) async {
Response response = await _dio.get(
Configuration.instance.getHttpEndpoint() + "/files/diff",
options:
@ -234,7 +230,7 @@ class PhotoSyncManager {
if (response != null) {
Bus.instance.fire(RemoteSyncEvent(true));
return (response.data["diff"] as List)
.map((photo) => new Photo.fromJson(photo))
.map((file) => new File.fromJson(file))
.toList();
} else {
Bus.instance.fire(RemoteSyncEvent(false));
@ -242,7 +238,7 @@ class PhotoSyncManager {
}
}
Future<Photo> _uploadFile(Photo localPhoto) async {
Future<File> _uploadFile(File localPhoto) async {
var title = getJPGFileNameForHEIC(localPhoto);
var formData = FormData.fromMap({
"file": MultipartFile.fromBytes((await localPhoto.getBytes()),
@ -260,25 +256,25 @@ class PhotoSyncManager {
data: formData,
)
.then((response) {
return Photo.fromJson(response.data);
return File.fromJson(response.data);
});
}
Future<void> _deletePhotosOnServer() async {
return _db.getAllDeletedPhotos().then((deletedPhotos) async {
for (Photo deletedPhoto in deletedPhotos) {
await _deletePhotoOnServer(deletedPhoto);
await _db.deletePhoto(deletedPhoto);
return _db.getAllDeleted().then((deletedPhotos) async {
for (File deletedPhoto in deletedPhotos) {
await _deleteFileOnServer(deletedPhoto);
await _db.delete(deletedPhoto);
}
});
}
Future<void> _deletePhotoOnServer(Photo photo) async {
Future<void> _deleteFileOnServer(File file) async {
return _dio
.delete(
Configuration.instance.getHttpEndpoint() +
"/files/" +
photo.uploadedFileId.toString(),
file.uploadedFileId.toString(),
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
@ -291,10 +287,10 @@ class PhotoSyncManager {
.createSync(recursive: true);
}
Future<bool> _insertPhotosToDB(
List<Photo> photos, SharedPreferences prefs, int timestamp) async {
await _db.insertPhotos(photos);
_logger.info("Inserted " + photos.length.toString() + " photos.");
Future<bool> _insertFilesToDB(
List<File> files, SharedPreferences prefs, int timestamp) async {
await _db.insertMultiple(files);
_logger.info("Inserted " + files.length.toString() + " files.");
return await prefs.setInt(_lastDBUpdateTimestampKey, timestamp);
}
}

View file

@ -3,17 +3,19 @@ import 'package:flutter/material.dart';
import 'package:like_button/like_button.dart';
import 'package:photos/core/cache/image_cache.dart';
import 'package:photos/favorite_photos_repository.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/file.dart';
import 'package:photos/ui/video_widget.dart';
import 'package:photos/ui/zoomable_image.dart';
import 'package:photos/utils/date_time_util.dart';
import 'package:photos/utils/share_util.dart';
import 'package:logging/logging.dart';
class DetailPage extends StatefulWidget {
final List<Photo> photos;
final List<File> files;
final int selectedIndex;
DetailPage(this.photos, this.selectedIndex, {key}) : super(key: key);
DetailPage(this.files, this.selectedIndex, {key}) : super(key: key);
@override
_DetailPageState createState() => _DetailPageState();
@ -22,12 +24,12 @@ class DetailPage extends StatefulWidget {
class _DetailPageState extends State<DetailPage> {
final _logger = Logger("DetailPageState");
bool _shouldDisableScroll = false;
List<Photo> _photos;
List<File> _files;
int _selectedIndex = 0;
@override
void initState() {
_photos = widget.photos;
_files = widget.files;
_selectedIndex = widget.selectedIndex;
super.initState();
}
@ -35,12 +37,12 @@ class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
_logger.info("Opening " +
_photos[_selectedIndex].toString() +
_files[_selectedIndex].toString() +
". " +
_selectedIndex.toString() +
" / " +
_photos.length.toString() +
" photos .");
_files.length.toString() +
" files .");
return Scaffold(
appBar: _buildAppBar(),
body: Center(
@ -54,52 +56,59 @@ class _DetailPageState extends State<DetailPage> {
Widget _buildPageView() {
return PageView.builder(
itemBuilder: (context, index) {
final photo = _photos[index];
final image = ZoomableImage(
photo,
final file = _files[index];
Widget content;
if (file.fileType == FileType.image) {
content = ZoomableImage(
file,
shouldDisableScroll: (value) {
setState(() {
_shouldDisableScroll = value;
});
},
);
_preloadPhotos(index);
return image;
} else if (file.fileType == FileType.video) {
content = VideoWidget(file);
} else {
content = Icon(Icons.error);
}
_preloadFiles(index);
return content;
},
onPageChanged: (index) {
setState(() {
_selectedIndex = index;
});
_preloadPhotos(index);
_preloadFiles(index);
},
physics: _shouldDisableScroll
? NeverScrollableScrollPhysics()
: PageScrollPhysics(),
controller: PageController(initialPage: _selectedIndex),
itemCount: _photos.length,
itemCount: _files.length,
);
}
void _preloadPhotos(int index) {
void _preloadFiles(int index) {
if (index > 0) {
_preloadPhoto(_photos[index - 1]);
_preloadFile(_files[index - 1]);
}
if (index < _photos.length - 1) {
_preloadPhoto(_photos[index + 1]);
if (index < _files.length - 1) {
_preloadFile(_files[index + 1]);
}
}
void _preloadPhoto(Photo photo) {
if (photo.localId == null) {
photo.getBytes().then((data) {
BytesLruCache.put(photo, data);
void _preloadFile(File file) {
if (file.localId == null) {
file.getBytes().then((data) {
BytesLruCache.put(file, data);
});
} else {
final cachedFile = FileLruCache.get(photo);
final cachedFile = FileLruCache.get(file);
if (cachedFile == null) {
photo.getAsset().then((asset) {
asset.file.then((file) {
FileLruCache.put(photo, file);
file.getAsset().then((asset) {
asset.file.then((assetFile) {
FileLruCache.put(file, assetFile);
});
});
}
@ -108,7 +117,7 @@ class _DetailPageState extends State<DetailPage> {
AppBar _buildAppBar() {
final actions = List<Widget>();
if (_photos[_selectedIndex].localId != null) {
if (_files[_selectedIndex].localId != null) {
actions.add(_getFavoriteButton());
}
actions.add(PopupMenuButton(
@ -142,9 +151,9 @@ class _DetailPageState extends State<DetailPage> {
},
onSelected: (value) {
if (value == 1) {
share(_photos[_selectedIndex]);
share(_files[_selectedIndex]);
} else if (value == 2) {
_displayInfo(_photos[_selectedIndex]);
_displayInfo(_files[_selectedIndex]);
}
},
));
@ -154,31 +163,27 @@ class _DetailPageState extends State<DetailPage> {
}
Widget _getFavoriteButton() {
final photo = _photos[_selectedIndex];
final file = _files[_selectedIndex];
return LikeButton(
isLiked: FavoritePhotosRepository.instance.isLiked(photo),
isLiked: FavoriteFilesRepository.instance.isLiked(file),
onTap: (oldValue) {
return FavoritePhotosRepository.instance.setLiked(photo, !oldValue);
return FavoriteFilesRepository.instance.setLiked(file, !oldValue);
},
);
}
Future<void> _displayInfo(Photo photo) async {
final asset = await photo.getAsset();
Future<void> _displayInfo(File file) async {
final asset = await file.getAsset();
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(photo.title),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
var items = <Widget>[
Row(
children: [
Icon(Icons.timer),
Padding(padding: EdgeInsets.all(4)),
Text(getFormattedTime(DateTime.fromMicrosecondsSinceEpoch(
photo.createTimestamp))),
Text(getFormattedTime(
DateTime.fromMicrosecondsSinceEpoch(file.createTimestamp))),
],
),
Padding(padding: EdgeInsets.all(4)),
@ -186,20 +191,33 @@ class _DetailPageState extends State<DetailPage> {
children: [
Icon(Icons.folder),
Padding(padding: EdgeInsets.all(4)),
Text(photo.deviceFolder),
Text(file.deviceFolder),
],
),
Padding(padding: EdgeInsets.all(4)),
Row(
];
if (file.fileType == FileType.image) {
items.add(Row(
children: [
Icon(Icons.photo_size_select_actual),
Padding(padding: EdgeInsets.all(4)),
Text(asset.width.toString() +
" x " +
asset.height.toString()),
Text(asset.width.toString() + " x " + asset.height.toString()),
],
),
));
} else {
items.add(Row(
children: [
Icon(Icons.timer),
Padding(padding: EdgeInsets.all(4)),
Text(asset.videoDuration.toString()),
],
));
}
return AlertDialog(
title: Text(file.title),
content: SingleChildScrollView(
child: ListBody(
children: items,
),
),
actions: <Widget>[

View file

@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/device_folder.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/models/file.dart';
import 'package:photos/file_repository.dart';
import 'package:photos/ui/gallery.dart';
import 'package:photos/ui/gallery_app_bar_widget.dart';
import 'package:logging/logging.dart';
@ -21,7 +21,7 @@ class DeviceFolderPage extends StatefulWidget {
class _DeviceFolderPageState extends State<DeviceFolderPage> {
final logger = Logger("DeviceFolderPageState");
Set<Photo> _selectedPhotos = Set<Photo>();
Set<File> _selectedFiles = Set<File>();
StreamSubscription<LocalPhotosUpdatedEvent> _subscription;
@override
@ -38,34 +38,34 @@ class _DeviceFolderPageState extends State<DeviceFolderPage> {
appBar: GalleryAppBarWidget(
GalleryAppBarType.local_folder,
widget.folder.name,
widget.folder.thumbnailPhoto.deviceFolder,
_selectedPhotos,
widget.folder.thumbnail.deviceFolder,
_selectedFiles,
onSelectionClear: () {
setState(() {
_selectedPhotos.clear();
_selectedFiles.clear();
});
},
),
body: Gallery(
() => Future.value(_getFilteredPhotos(PhotoRepository.instance.photos)),
selectedPhotos: _selectedPhotos,
onPhotoSelectionChange: (Set<Photo> selectedPhotos) {
() => Future.value(_getFilteredFiles(FileRepository.instance.files)),
selectedFiles: _selectedFiles,
onFileSelectionChange: (Set<File> selectedFiles) {
setState(() {
_selectedPhotos = selectedPhotos;
_selectedFiles = selectedFiles;
});
},
),
);
}
List<Photo> _getFilteredPhotos(List<Photo> unfilteredPhotos) {
final List<Photo> filteredPhotos = List<Photo>();
for (Photo photo in unfilteredPhotos) {
if (widget.folder.filter.shouldInclude(photo)) {
filteredPhotos.add(photo);
List<File> _getFilteredFiles(List<File> unfilteredFiles) {
final List<File> filteredFiles = List<File>();
for (File file in unfilteredFiles) {
if (widget.folder.filter.shouldInclude(file)) {
filteredFiles.add(file);
}
}
return filteredPhotos;
return filteredFiles;
}
@override

View file

@ -68,23 +68,23 @@ class _DeviceFolderGalleryWidgetState extends State<DeviceFolderGalleryWidget> {
}
Future<List<DeviceFolder>> _getDeviceFolders() async {
final paths = await PhotoDB.instance.getLocalPaths();
final paths = await FileDB.instance.getLocalPaths();
final folders = List<DeviceFolder>();
for (final path in paths) {
final photo = await PhotoDB.instance.getLatestPhotoInPath(path);
final file = await FileDB.instance.getLatestFileInPath(path);
final folderName = p.basename(path);
folders
.add(DeviceFolder(folderName, photo, FolderNameFilter(folderName)));
.add(DeviceFolder(folderName, file, FolderNameFilter(folderName)));
}
folders.sort((first, second) {
return second.thumbnailPhoto.createTimestamp
.compareTo(first.thumbnailPhoto.createTimestamp);
return second.thumbnail.createTimestamp
.compareTo(first.thumbnail.createTimestamp);
});
if (FavoritePhotosRepository.instance.hasFavorites()) {
final photo = await PhotoDB.instance.getLatestPhotoAmongGeneratedIds(
FavoritePhotosRepository.instance.getLiked().toList());
if (FavoriteFilesRepository.instance.hasFavorites()) {
final file = await FileDB.instance.getLatestFileAmongGeneratedIds(
FavoriteFilesRepository.instance.getLiked().toList());
folders.insert(
0, DeviceFolder("Favorites", photo, FavoriteItemsFilter()));
0, DeviceFolder("Favorites", file, FavoriteItemsFilter()));
}
return folders;
}
@ -94,7 +94,7 @@ class _DeviceFolderGalleryWidgetState extends State<DeviceFolderGalleryWidget> {
child: Column(
children: <Widget>[
Container(
child: ThumbnailWidget(folder.thumbnailPhoto),
child: ThumbnailWidget(folder.thumbnail),
height: 150,
width: 150,
),

View file

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:photos/events/event.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:photos/ui/detail_page.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/sync_indicator.dart';
@ -15,20 +15,20 @@ import 'package:photos/utils/date_time_util.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class Gallery extends StatefulWidget {
final Future<List<Photo>> Function() loader;
final Future<List<File>> Function() loader;
// TODO: Verify why the event is necessary when calling loader post onRefresh
// should have done the job.
final Stream<Event> reloadEvent;
final Future<void> Function() onRefresh;
final Set<Photo> selectedPhotos;
final Function(Set<Photo>) onPhotoSelectionChange;
final Set<File> selectedFiles;
final Function(Set<File>) onFileSelectionChange;
Gallery(
this.loader, {
this.reloadEvent,
this.onRefresh,
this.selectedPhotos,
this.onPhotoSelectionChange,
this.selectedFiles,
this.onFileSelectionChange,
});
@override
@ -40,12 +40,12 @@ class Gallery extends StatefulWidget {
class _GalleryState extends State<Gallery> {
final Logger _logger = Logger("Gallery");
final ScrollController _scrollController = ScrollController();
final List<List<Photo>> _collatedPhotos = List<List<Photo>>();
final List<List<File>> _collatedFiles = List<List<File>>();
bool _requiresLoad = false;
AsyncSnapshot<List<Photo>> _lastSnapshot;
Set<Photo> _selectedPhotos = HashSet<Photo>();
List<Photo> _photos;
AsyncSnapshot<List<File>> _lastSnapshot;
Set<File> _selectedFiles = HashSet<File>();
List<File> _files;
RefreshController _refreshController = RefreshController();
@override
@ -66,7 +66,7 @@ class _GalleryState extends State<Gallery> {
if (!_requiresLoad) {
return _onSnapshotAvailable(_lastSnapshot);
}
return FutureBuilder<List<Photo>>(
return FutureBuilder<List<File>>(
future: widget.loader(),
builder: (context, snapshot) {
_lastSnapshot = snapshot;
@ -75,7 +75,7 @@ class _GalleryState extends State<Gallery> {
);
}
Widget _onSnapshotAvailable(AsyncSnapshot<List<Photo>> snapshot) {
Widget _onSnapshotAvailable(AsyncSnapshot<List<File>> snapshot) {
if (snapshot.hasData) {
_requiresLoad = false;
return _onDataLoaded(snapshot.data);
@ -87,15 +87,15 @@ class _GalleryState extends State<Gallery> {
}
}
Widget _onDataLoaded(List<Photo> photos) {
_photos = photos;
if (_photos.isEmpty) {
Widget _onDataLoaded(List<File> files) {
_files = files;
if (_files.isEmpty) {
return Center(child: Text("Nothing to see here! 👀"));
}
_selectedPhotos = widget.selectedPhotos ?? Set<Photo>();
_collatePhotos();
_selectedFiles = widget.selectedFiles ?? Set<File>();
_collateFiles();
final list = ListView.builder(
itemCount: _collatedPhotos.length,
itemCount: _collatedFiles.length,
itemBuilder: _buildListItem,
controller: _scrollController,
cacheExtent: 1000,
@ -123,12 +123,9 @@ class _GalleryState extends State<Gallery> {
}
Widget _buildListItem(BuildContext context, int index) {
var photos = _collatedPhotos[index];
var files = _collatedFiles[index];
return Column(
children: <Widget>[
_getDay(photos[0].createTimestamp),
_getGallery(photos)
],
children: <Widget>[_getDay(files[0].createTimestamp), _getGallery(files)],
);
}
@ -143,64 +140,64 @@ class _GalleryState extends State<Gallery> {
);
}
Widget _getGallery(List<Photo> photos) {
Widget _getGallery(List<File> files) {
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 12),
physics: ScrollPhysics(), // to disable GridView's scrolling
itemBuilder: (context, index) {
return _buildPhoto(context, photos[index]);
return _buildFile(context, files[index]);
},
itemCount: photos.length,
itemCount: files.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
);
}
Widget _buildPhoto(BuildContext context, Photo photo) {
Widget _buildFile(BuildContext context, File file) {
return GestureDetector(
onTap: () {
if (_selectedPhotos.isNotEmpty) {
_selectPhoto(photo);
if (_selectedFiles.isNotEmpty) {
_selectFile(file);
} else {
_routeToDetailPage(photo, context);
_routeToDetailPage(file, context);
}
},
onLongPress: () {
HapticFeedback.lightImpact();
_selectPhoto(photo);
_selectFile(file);
},
child: Container(
margin: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
border: _selectedPhotos.contains(photo)
border: _selectedFiles.contains(file)
? Border.all(width: 4.0, color: Colors.blue)
: null,
),
child: Hero(
tag: photo.generatedId.toString(),
child: ThumbnailWidget(photo),
tag: file.generatedId.toString(),
child: ThumbnailWidget(file),
),
),
);
}
void _selectPhoto(Photo photo) {
void _selectFile(File file) {
setState(() {
if (_selectedPhotos.contains(photo)) {
_selectedPhotos.remove(photo);
if (_selectedFiles.contains(file)) {
_selectedFiles.remove(file);
} else {
_selectedPhotos.add(photo);
_selectedFiles.add(file);
}
widget.onPhotoSelectionChange(_selectedPhotos);
widget.onFileSelectionChange(_selectedFiles);
});
}
void _routeToDetailPage(Photo photo, BuildContext context) {
void _routeToDetailPage(File file, BuildContext context) {
final page = DetailPage(
_photos,
_photos.indexOf(photo),
_files,
_files.indexOf(file),
);
Navigator.of(context).push(
MaterialPageRoute(
@ -211,31 +208,30 @@ class _GalleryState extends State<Gallery> {
);
}
void _collatePhotos() {
final dailyPhotos = List<Photo>();
final collatedPhotos = List<List<Photo>>();
for (int index = 0; index < _photos.length; index++) {
void _collateFiles() {
final dailyFiles = List<File>();
final collatedFiles = List<List<File>>();
for (int index = 0; index < _files.length; index++) {
if (index > 0 &&
!_arePhotosFromSameDay(_photos[index], _photos[index - 1])) {
var collatedDailyPhotos = List<Photo>();
collatedDailyPhotos.addAll(dailyPhotos);
collatedPhotos.add(collatedDailyPhotos);
dailyPhotos.clear();
!_areFilesFromSameDay(_files[index], _files[index - 1])) {
var collatedDailyFiles = List<File>();
collatedDailyFiles.addAll(dailyFiles);
collatedFiles.add(collatedDailyFiles);
dailyFiles.clear();
}
dailyPhotos.add(_photos[index]);
dailyFiles.add(_files[index]);
}
if (dailyPhotos.isNotEmpty) {
collatedPhotos.add(dailyPhotos);
if (dailyFiles.isNotEmpty) {
collatedFiles.add(dailyFiles);
}
_collatedPhotos.clear();
_collatedPhotos.addAll(collatedPhotos);
_collatedFiles.clear();
_collatedFiles.addAll(collatedFiles);
}
bool _arePhotosFromSameDay(Photo firstPhoto, Photo secondPhoto) {
var firstDate =
DateTime.fromMicrosecondsSinceEpoch(firstPhoto.createTimestamp);
bool _areFilesFromSameDay(File first, File second) {
var firstDate = DateTime.fromMicrosecondsSinceEpoch(first.createTimestamp);
var secondDate =
DateTime.fromMicrosecondsSinceEpoch(secondPhoto.createTimestamp);
DateTime.fromMicrosecondsSinceEpoch(second.createTimestamp);
return firstDate.year == secondDate.year &&
firstDate.month == secondDate.month &&
firstDate.day == secondDate.day;

View file

@ -5,8 +5,8 @@ import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/photo_db.dart';
import 'package:photos/events/remote_sync_event.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/models/file.dart';
import 'package:photos/file_repository.dart';
import 'package:photos/ui/setup_page.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/ui/share_folder_widget.dart';
@ -23,10 +23,10 @@ class GalleryAppBarWidget extends StatefulWidget
final GalleryAppBarType type;
final String title;
final String path;
final Set<Photo> selectedPhotos;
final Set<File> selectedFiles;
final Function() onSelectionClear;
GalleryAppBarWidget(this.type, this.title, this.path, this.selectedPhotos,
GalleryAppBarWidget(this.type, this.title, this.path, this.selectedFiles,
{this.onSelectionClear});
@override
@ -52,7 +52,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
@override
Widget build(BuildContext context) {
if (widget.selectedPhotos.isEmpty) {
if (widget.selectedFiles.isEmpty) {
return AppBar(
title: Text(widget.title),
actions: _getDefaultActions(context),
@ -63,11 +63,11 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
leading: IconButton(
icon: Icon(Icons.close),
onPressed: () {
_clearSelectedPhotos();
_clearSelectedFiles();
},
),
title: Text(widget.selectedPhotos.length.toString()),
actions: _getPhotoActions(context),
title: Text(widget.selectedFiles.length.toString()),
actions: _getActions(context),
);
}
@ -103,46 +103,46 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
);
}
List<Widget> _getPhotoActions(BuildContext context) {
List<Widget> _getActions(BuildContext context) {
List<Widget> actions = List<Widget>();
if (widget.selectedPhotos.isNotEmpty) {
if (widget.selectedFiles.isNotEmpty) {
if (widget.type != GalleryAppBarType.remote_folder) {
actions.add(IconButton(
icon: Icon(Icons.delete),
onPressed: () {
_showDeletePhotosSheet(context);
_showDeleteSheet(context);
},
));
}
actions.add(IconButton(
icon: Icon(Icons.share),
onPressed: () {
_shareSelectedPhotos(context);
_shareSelected(context);
},
));
}
return actions;
}
void _shareSelectedPhotos(BuildContext context) {
shareMultiple(widget.selectedPhotos.toList());
void _shareSelected(BuildContext context) {
shareMultiple(widget.selectedFiles.toList());
}
void _showDeletePhotosSheet(BuildContext context) {
void _showDeleteSheet(BuildContext context) {
final action = CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(
child: Text("Delete on device"),
isDestructiveAction: true,
onPressed: () async {
await _deleteSelectedPhotos(context, false);
await _deleteSelected(context, false);
},
),
CupertinoActionSheetAction(
child: Text("Delete everywhere [WiP]"),
isDestructiveAction: true,
onPressed: () async {
await _deleteSelectedPhotos(context, true);
await _deleteSelected(context, true);
},
)
],
@ -156,24 +156,23 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
showCupertinoModalPopup(context: context, builder: (_) => action);
}
Future _deleteSelectedPhotos(
BuildContext context, bool deleteEverywhere) async {
Future _deleteSelected(BuildContext context, bool deleteEverywhere) async {
await PhotoManager.editor
.deleteWithIds(widget.selectedPhotos.map((p) => p.localId).toList());
.deleteWithIds(widget.selectedFiles.map((p) => p.localId).toList());
for (Photo photo in widget.selectedPhotos) {
for (File file in widget.selectedFiles) {
deleteEverywhere
? await PhotoDB.instance.markPhotoForDeletion(photo)
: await PhotoDB.instance.deletePhoto(photo);
? await FileDB.instance.markForDeletion(file)
: await FileDB.instance.delete(file);
}
Navigator.of(context, rootNavigator: true).pop();
PhotoRepository.instance.reloadPhotos();
_clearSelectedPhotos();
FileRepository.instance.reloadFiles();
_clearSelectedFiles();
}
void _clearSelectedPhotos() {
void _clearSelectedFiles() {
setState(() {
widget.selectedPhotos.clear();
widget.selectedFiles.clear();
});
if (widget.onSelectionClear != null) {
widget.onSelectionClear();

View file

@ -7,8 +7,8 @@ import 'package:flutter/widgets.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/filters/important_items_filter.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/models/file.dart';
import 'package:photos/file_repository.dart';
import 'package:photos/photo_sync_manager.dart';
import 'package:photos/ui/device_folders_gallery_widget.dart';
import 'package:photos/ui/gallery.dart';
@ -38,7 +38,7 @@ class _HomeWidgetState extends State<HomeWidget> {
ShakeDetector _detector;
int _selectedNavBarItem = 0;
Set<Photo> _selectedPhotos = HashSet<Photo>();
Set<File> _selectedPhotos = HashSet<File>();
StreamSubscription<LocalPhotosUpdatedEvent>
_localPhotosUpdatedEventSubscription;
@ -110,18 +110,18 @@ class _HomeWidgetState extends State<HomeWidget> {
Widget _getMainGalleryWidget() {
return FutureBuilder<bool>(
future: PhotoRepository.instance.loadPhotos(),
future: FileRepository.instance.loadFiles(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Gallery(
() => Future.value(
_getFilteredPhotos(PhotoRepository.instance.photos)),
() =>
Future.value(_getFilteredPhotos(FileRepository.instance.files)),
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
onRefresh: () {
return PhotoSyncManager.instance.sync();
},
selectedPhotos: _selectedPhotos,
onPhotoSelectionChange: (Set<Photo> selectedPhotos) {
selectedFiles: _selectedPhotos,
onFileSelectionChange: (Set<File> selectedPhotos) {
setState(() {
_selectedPhotos = selectedPhotos;
});
@ -162,11 +162,11 @@ class _HomeWidgetState extends State<HomeWidget> {
);
}
List<Photo> _getFilteredPhotos(List<Photo> unfilteredPhotos) {
final List<Photo> filteredPhotos = List<Photo>();
for (Photo photo in unfilteredPhotos) {
if (importantItemsFilter.shouldInclude(photo)) {
filteredPhotos.add(photo);
List<File> _getFilteredPhotos(List<File> unfilteredFiles) {
final List<File> filteredPhotos = List<File>();
for (File file in unfilteredFiles) {
if (importantItemsFilter.shouldInclude(file)) {
filteredPhotos.add(file);
}
}
return filteredPhotos;

View file

@ -3,10 +3,9 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:photos/models/location.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/models/file.dart';
import 'package:photos/file_repository.dart';
import 'package:photos/ui/gallery.dart';
import 'package:photos/ui/loading_widget.dart';
class ViewPort {
final Location northEast;
@ -45,25 +44,25 @@ class _LocationSearchResultsPageState extends State<LocationSearchResultsPage> {
);
}
FutureOr<List<Photo>> _getResult() async {
final photos = PhotoRepository.instance.photos;
FutureOr<List<File>> _getResult() async {
final files = FileRepository.instance.files;
final args = Map<String, dynamic>();
args['photos'] = photos;
args['files'] = files;
args['viewPort'] = widget.viewPort;
return _filterPhotos(args);
}
static List<Photo> _filterPhotos(Map<String, dynamic> args) {
List<Photo> photos = args['photos'];
static List<File> _filterPhotos(Map<String, dynamic> args) {
List<File> files = args['files'];
ViewPort viewPort = args['viewPort'];
final result = List<Photo>();
for (final photo in photos) {
if (photo.location != null &&
viewPort.northEast.latitude > photo.location.latitude &&
viewPort.southWest.latitude < photo.location.latitude &&
viewPort.northEast.longitude > photo.location.longitude &&
viewPort.southWest.longitude < photo.location.longitude) {
result.add(photo);
final result = List<File>();
for (final file in files) {
if (file.location != null &&
viewPort.northEast.latitude > file.location.latitude &&
viewPort.southWest.latitude < file.location.latitude &&
viewPort.northEast.longitude > file.location.longitude &&
viewPort.southWest.longitude < file.location.longitude) {
result.add(file);
} else {}
}
return result;

View file

@ -83,7 +83,7 @@ class _RemoteFolderGalleryWidgetState extends State<RemoteFolderGalleryWidget> {
}
try {
folder.thumbnailPhoto =
await PhotoDB.instance.getLatestPhotoInRemoteFolder(folder.id);
await FileDB.instance.getLatestFileInRemoteFolder(folder.id);
} catch (e) {
_logger.warning(e.toString());
}

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:photos/db/photo_db.dart';
import 'package:photos/folder_service.dart';
import 'package:photos/models/folder.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:photos/ui/gallery.dart';
import 'package:photos/ui/gallery_app_bar_widget.dart';
@ -16,7 +16,7 @@ class RemoteFolderPage extends StatefulWidget {
}
class _RemoteFolderPageState extends State<RemoteFolderPage> {
Set<Photo> _selectedPhotos = Set<Photo>();
Set<File> _selectedPhotos = Set<File>();
@override
Widget build(Object context) {
@ -32,12 +32,11 @@ class _RemoteFolderPageState extends State<RemoteFolderPage> {
});
},
),
body: Gallery(
() => PhotoDB.instance.getAllPhotosInFolder(widget.folder.id),
body: Gallery(() => FileDB.instance.getAllInFolder(widget.folder.id),
onRefresh: () =>
FolderSharingService.instance.syncDiff(widget.folder),
selectedPhotos: _selectedPhotos,
onPhotoSelectionChange: (Set<Photo> selectedPhotos) {
selectedFiles: _selectedPhotos,
onFileSelectionChange: (Set<File> selectedPhotos) {
setState(
() {
_selectedPhotos = selectedPhotos;

View file

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:photos/core/cache/thumbnail_cache.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/constants.dart';
class ThumbnailWidget extends StatefulWidget {
final Photo photo;
final File photo;
const ThumbnailWidget(
this.photo, {
@ -78,7 +78,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
}
void _loadNetworkImage() {
final url = widget.photo.thumbnailPath.isNotEmpty
final url = widget.photo.previewURL.isNotEmpty
? widget.photo.getThumbnailUrl()
: widget.photo.getRemoteUrl();
_imageProvider = Image.network(url).image;

64
lib/ui/video_widget.dart Normal file
View file

@ -0,0 +1,64 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/models/file.dart';
import 'package:video_player/video_player.dart';
import 'loading_widget.dart';
class VideoWidget extends StatefulWidget {
final File file;
VideoWidget(this.file, {Key key}) : super(key: key);
@override
_VideoWidgetState createState() => _VideoWidgetState();
}
class _VideoWidgetState extends State<VideoWidget> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
Future<void> _future;
@override
void initState() {
super.initState();
_future = _initVideoPlayer();
}
@override
void dispose() {
_videoPlayerController.dispose();
_chewieController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return loadWidget;
}
return Chewie(
controller: _chewieController,
);
},
);
}
Future<void> _initVideoPlayer() async {
final url = widget.file.localId == null
? widget.file.getRemoteUrl()
: await (await widget.file.getAsset()).getMediaUrl();
_videoPlayerController = VideoPlayerController.network(url);
await _videoPlayerController.initialize();
setState(() {
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
aspectRatio: _videoPlayerController.value.aspectRatio,
autoPlay: true,
autoInitialize: true,
);
});
}
}

View file

@ -3,13 +3,13 @@ import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/cache/image_cache.dart';
import 'package:photos/core/cache/thumbnail_cache.dart';
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photos/core/constants.dart';
class ZoomableImage extends StatefulWidget {
final Photo photo;
final File photo;
final Function(bool) shouldDisableScroll;
ZoomableImage(
@ -67,7 +67,7 @@ class _ZoomableImageState extends State<ZoomableImage>
}
void _loadNetworkImage() {
if (!_loadedSmallThumbnail && widget.photo.thumbnailPath.isNotEmpty) {
if (!_loadedSmallThumbnail && widget.photo.previewURL.isNotEmpty) {
_imageProvider = Image.network(
widget.photo.getThumbnailUrl(),
gaplessPlayback: true,

View file

@ -1,14 +1,14 @@
import 'package:photos/models/photo.dart';
import 'package:photos/models/file.dart';
import 'package:path/path.dart';
String getJPGFileNameForHEIC(Photo photo) {
return extension(photo.title) == ".HEIC"
? basenameWithoutExtension(photo.title) + ".JPG"
: photo.title;
String getJPGFileNameForHEIC(File file) {
return extension(file.title) == ".HEIC"
? basenameWithoutExtension(file.title) + ".JPG"
: file.title;
}
String getHEICFileNameForJPG(Photo photo) {
return extension(photo.title) == ".JPG"
? basenameWithoutExtension(photo.title) + ".HEIC"
: photo.title;
String getHEICFileNameForJPG(File file) {
return extension(file.title) == ".JPG"
? basenameWithoutExtension(file.title) + ".HEIC"
: file.title;
}

View file

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

View file

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
chewie:
dependency: "direct main"
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.10"
clock:
dependency: transitive
description:
@ -289,6 +296,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
open_iconic_flutter:
dependency: transitive
description:
name: open_iconic_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
package_info:
dependency: transitive
description:
@ -532,6 +546,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
video_player:
dependency: "direct main"
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.11+1"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3+1"
visibility_detector:
dependency: "direct main"
description:
@ -539,6 +574,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4+1"
xml:
dependency: transitive
description:

View file

@ -51,6 +51,8 @@ dependencies:
pull_to_refresh: ^1.5.7
fluttertoast: ^4.0.1
extended_image: ^0.9.0
video_player: ^0.10.11+1
chewie: ^0.9.10
dev_dependencies:
flutter_test: