Extend support for videos
This commit is contained in:
parent
1e92a0ad08
commit
a5aaf91460
35 changed files with 681 additions and 521 deletions
|
@ -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
|
||||
|
||||
|
|
22
lib/core/cache/image_cache.dart
vendored
22
lib/core/cache/image_cache.dart
vendored
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
8
lib/core/cache/thumbnail_cache.dart
vendored
8
lib/core/cache/thumbnail_cache.dart
vendored
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
33
lib/file_repository.dart
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
16
lib/models/file_type.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" ||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>[
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
64
lib/ui/video_widget.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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, "*/*");
|
||||
}
|
||||
|
|
42
pubspec.lock
42
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue