Update to latest API

This commit is contained in:
Vishnu Mohandas 2020-05-17 18:09:38 +05:30
parent 7cc3803242
commit c04f2baa3c
16 changed files with 345 additions and 167 deletions

View file

@ -0,0 +1,46 @@
import 'package:dio/dio.dart';
import 'package:photos/core/configuration.dart';
class AlbumSharingService {
final _dio = Dio();
AlbumSharingService._privateConstructor();
static final AlbumSharingService instance =
AlbumSharingService._privateConstructor();
Future<Map<String, bool>> getSharingStatus(String path) async {
// TODO fetch folderID from path
var folderID = 0;
return _dio
.get(
Configuration.instance.getHttpEndpoint() + "/users",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((usersResponse) {
return _dio
.get(
Configuration.instance.getHttpEndpoint() +
"/folders/" +
folderID.toString(),
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((sharedFoldersResponse) {
final sharedUsers =
(sharedFoldersResponse.data["sharedWith"] as List).toSet();
final result = Map<String, bool>();
(usersResponse.data as List).forEach((user) {
if (user != Configuration.instance.getUsername()) {
result[user] = sharedUsers.contains(user);
}
});
return result;
});
});
}
void shareAlbum(
String path,
) {}
}

View file

@ -15,7 +15,7 @@ class DatabaseHelper {
static final columnUploadedFileId = 'uploaded_file_id';
static final columnLocalId = 'local_id';
static final columnTitle = 'title';
static final columnPathName = 'path_name';
static final columnDeviceFolder = 'device_folder';
static final columnRemotePath = 'remote_path';
static final columnIsDeleted = 'is_deleted';
static final columnCreateTimestamp = 'create_timestamp';
@ -50,7 +50,7 @@ class DatabaseHelper {
$columnLocalId TEXT,
$columnUploadedFileId INTEGER NOT NULL,
$columnTitle TEXT NOT NULL,
$columnPathName TEXT NOT NULL,
$columnDeviceFolder TEXT NOT NULL,
$columnRemotePath TEXT,
$columnIsDeleted INTEGER DEFAULT 0,
$columnCreateTimestamp TEXT NOT NULL,
@ -161,12 +161,12 @@ class DatabaseHelper {
final db = await instance.database;
final rows = await db.query(
table,
columns: [columnPathName],
columns: [columnDeviceFolder],
distinct: true,
);
List<String> result = List<String>();
for (final row in rows) {
result.add(row[columnPathName]);
result.add(row[columnDeviceFolder]);
}
return result;
}
@ -175,7 +175,7 @@ class DatabaseHelper {
final db = await instance.database;
var rows = await db.query(
table,
where: '$columnPathName =?',
where: '$columnDeviceFolder =?',
whereArgs: [path],
orderBy: '$columnCreateTimestamp DESC',
limit: 1,
@ -217,7 +217,7 @@ class DatabaseHelper {
row[columnUploadedFileId] =
photo.uploadedFileId == null ? -1 : photo.uploadedFileId;
row[columnTitle] = photo.title;
row[columnPathName] = photo.pathName;
row[columnDeviceFolder] = photo.deviceFolder;
row[columnRemotePath] = photo.remotePath;
row[columnCreateTimestamp] = photo.createTimestamp;
row[columnSyncTimestamp] = photo.syncTimestamp;
@ -230,7 +230,7 @@ class DatabaseHelper {
photo.localId = row[columnLocalId];
photo.uploadedFileId = row[columnUploadedFileId];
photo.title = row[columnTitle];
photo.pathName = row[columnPathName];
photo.deviceFolder = row[columnDeviceFolder];
photo.remotePath = row[columnRemotePath];
photo.createTimestamp = int.parse(row[columnCreateTimestamp]);
photo.syncTimestamp = row[columnSyncTimestamp] == null

View file

@ -15,7 +15,7 @@ final logger = Logger("main");
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SuperLogging.main(LogConfig(
await SuperLogging.main(LogConfig(
body: _main,
sentryDsn: SENTRY_DSN,
logDirPath: (await getTemporaryDirectory()).path + "/logs",

View file

@ -9,6 +9,6 @@ class FolderNameFilter implements GalleryItemsFilter {
@override
bool shouldInclude(Photo photo) {
return path.basename(photo.pathName) == folderName;
return path.basename(photo.deviceFolder) == folderName;
}
}

View file

@ -8,7 +8,7 @@ class ImportantItemsFilter implements GalleryItemsFilter {
@override
bool shouldInclude(Photo photo) {
if (Platform.isAndroid) {
final String folder = basename(photo.pathName);
final String folder = basename(photo.deviceFolder);
return folder == "Camera" ||
folder == "DCIM" ||
folder == "Download" ||

11
lib/models/folder.dart Normal file
View file

@ -0,0 +1,11 @@
class Folder {
final int folderID;
final String name;
final String owner;
final String deviceFolder;
final List<String> sharedWith;
final int updateTimestamp;
Folder(this.folderID, this.name, this.owner, this.deviceFolder,
this.sharedWith, this.updateTimestamp);
}

View file

@ -10,7 +10,7 @@ class Photo {
int uploadedFileId;
String localId;
String title;
String pathName;
String deviceFolder;
String remotePath;
int createTimestamp;
int syncTimestamp;
@ -18,8 +18,9 @@ class Photo {
Photo();
Photo.fromJson(Map<String, dynamic> json)
: uploadedFileId = json["fileId"],
remotePath = json["path"],
title = json["title"],
deviceFolder = json["deviceFolder"],
remotePath = json["path"],
createTimestamp = json["createTimestamp"],
syncTimestamp = json["syncTimestamp"];
@ -29,7 +30,7 @@ class Photo {
photo.uploadedFileId = -1;
photo.localId = asset.id;
photo.title = asset.title;
photo.pathName = pathEntity.name;
photo.deviceFolder = pathEntity.name;
photo.createTimestamp = asset.createDateTime.microsecondsSinceEpoch;
if (photo.createTimestamp == 0) {
try {
@ -67,7 +68,7 @@ class Photo {
@override
String toString() {
return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, title: $title, pathName: $pathName, remotePath: $remotePath, createTimestamp: $createTimestamp, syncTimestamp: $syncTimestamp)';
return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, title: $title, deviceFolder: $deviceFolder, remotePath: $remotePath, createTimestamp: $createTimestamp, syncTimestamp: $syncTimestamp)';
}
@override
@ -79,7 +80,7 @@ class Photo {
o.uploadedFileId == uploadedFileId &&
o.localId == localId &&
o.title == title &&
o.pathName == pathName &&
o.deviceFolder == deviceFolder &&
o.remotePath == remotePath &&
o.createTimestamp == createTimestamp &&
o.syncTimestamp == syncTimestamp;
@ -91,7 +92,7 @@ class Photo {
uploadedFileId.hashCode ^
localId.hashCode ^
title.hashCode ^
pathName.hashCode ^
deviceFolder.hashCode ^
remotePath.hashCode ^
createTimestamp.hashCode ^
syncTimestamp.hashCode;

View file

@ -8,7 +8,6 @@ import 'package:photos/db/db_helper.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/photo_provider.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -110,15 +109,13 @@ class PhotoSyncManager {
await _getDiff(lastSyncTimestamp, _diffLimit).then((diff) async {
if (diff != null) {
await _downloadDiff(diff, prefs).then((_) {
if (diff.length > 0) {
_syncPhotos();
}
await _storeDiff(diff, prefs).then((_) {
// TODO: Recursively store diff
_uploadDiff(prefs);
});
}
});
_uploadDiff(prefs);
// TODO: Fix race conditions triggered due to concurrent syncs.
// Add device_id/last_sync_timestamp to the upload request?
}
@ -127,32 +124,26 @@ class PhotoSyncManager {
List<Photo> photosToBeUploaded =
await DatabaseHelper.instance.getPhotosToBeUploaded();
for (Photo photo in photosToBeUploaded) {
var uploadedPhoto = await _uploadFile(photo);
if (uploadedPhoto == null) {
return;
try {
var uploadedPhoto = await _uploadFile(photo);
if (uploadedPhoto == null) {
return;
}
await DatabaseHelper.instance.updatePhoto(photo.generatedId,
uploadedPhoto.remotePath, uploadedPhoto.syncTimestamp);
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp);
} catch (e) {
_logger.severe(e);
}
await DatabaseHelper.instance.updatePhoto(photo.generatedId,
uploadedPhoto.remotePath, uploadedPhoto.syncTimestamp);
prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp);
}
}
Future _downloadDiff(List<Photo> diff, SharedPreferences prefs) async {
var externalPath = (await getApplicationDocumentsDirectory()).path;
var path = externalPath + "/photos/";
Future _storeDiff(List<Photo> diff, SharedPreferences prefs) async {
for (Photo photo in diff) {
var localPath = path + basename(photo.remotePath);
await _dio
.download(
Configuration.instance.getHttpEndpoint() + "/" + photo.remotePath,
localPath)
.catchError((e) => _logger.severe(e));
// TODO: Save path
photo.pathName = localPath;
await DatabaseHelper.instance.insertPhoto(photo);
PhotoRepository.instance.reloadPhotos();
await prefs.setInt(_lastSyncTimestampKey, photo.syncTimestamp);
}
PhotoRepository.instance.reloadPhotos();
}
Future<List<Photo>> _getDiff(int lastSyncTimestamp, int limit) async {
@ -180,19 +171,22 @@ class PhotoSyncManager {
var formData = FormData.fromMap({
"file": MultipartFile.fromBytes((await localPhoto.getOriginalBytes()),
filename: localPhoto.title),
"deviceFolder": localPhoto.deviceFolder,
"title": localPhoto.title,
"createTimestamp": localPhoto.createTimestamp,
"token": Configuration.instance.getToken(),
});
return _dio
.post(
Configuration.instance.getHttpEndpoint() + "/files",
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
data: formData,
)
.then((response) => Photo.fromJson(response.data))
.catchError((e) => _logger.severe(e));
Configuration.instance.getHttpEndpoint() + "/files",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
data: formData,
)
.then((response) {
return Photo.fromJson(response.data);
}).catchError((e) {
_logger.severe("Error in uploading ", e);
});
}
Future<void> _deletePhotos() async {

View file

@ -37,6 +37,7 @@ class _AlbumPageState extends State<AlbumPage> {
return Scaffold(
appBar: GalleryAppBarWidget(
widget.album.name,
widget.album.thumbnailPhoto.deviceFolder,
_selectedPhotos,
onSelectionClear: () {
setState(() {

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
@ -9,15 +8,18 @@ import 'package:photos/models/photo.dart';
import 'package:photos/photo_repository.dart';
import 'package:photos/ui/setup_page.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/ui/share_album_widget.dart';
import 'package:photos/utils/share_util.dart';
class GalleryAppBarWidget extends StatefulWidget
implements PreferredSizeWidget {
final String title;
final String path;
final Set<Photo> selectedPhotos;
final Function() onSelectionClear;
GalleryAppBarWidget(this.title, this.selectedPhotos, {this.onSelectionClear});
GalleryAppBarWidget(this.title, this.path, this.selectedPhotos,
{this.onSelectionClear});
@override
_GalleryAppBarWidgetState createState() => _GalleryAppBarWidgetState();
@ -70,10 +72,26 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
_openSyncConfiguration(context);
},
));
} else {
actions.add(IconButton(
icon: Icon(Icons.person_add),
onPressed: () {
_showShareAlbumDialog();
},
));
}
return actions;
}
Future<void> _showShareAlbumDialog() async {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return ShareAlbumWidget(widget.title, widget.path);
},
);
}
List<Widget> _getPhotoActions(BuildContext context) {
List<Widget> actions = List<Widget>();
if (widget.selectedPhotos.isNotEmpty) {

View file

@ -53,6 +53,7 @@ class _HomeWidgetState extends State<HomeWidget> {
return Scaffold(
appBar: GalleryAppBarWidget(
widget.title,
"/",
_selectedPhotos,
onSelectionClear: _clearSelectedPhotos,
),

View file

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/album_sharing_service.dart';
import 'package:photos/ui/loading_widget.dart';
class ShareAlbumWidget extends StatefulWidget {
final String title;
final String path;
const ShareAlbumWidget(
this.title,
this.path, {
Key key,
}) : super(key: key);
@override
_ShareAlbumWidgetState createState() => _ShareAlbumWidgetState();
}
class _ShareAlbumWidgetState extends State<ShareAlbumWidget> {
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, bool>>(
future: AlbumSharingService.instance.getSharingStatus(widget.path),
builder: (context, snapshot) {
if (snapshot.hasData) {
return _getSharingDialog(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return loadWidget;
}
},
);
}
Widget _getSharingDialog(Map<String, bool> sharingStatus) {
return AlertDialog(
title: Text('Share "' + widget.title + '" with'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
SharingCheckboxWidget(sharingStatus),
],
),
),
actions: <Widget>[
FlatButton(
child: Text("Share"),
onPressed: () {
// TODO: AlbumSharingService.instance.shareAlbum();
Navigator.of(context).pop();
},
),
],
);
}
}
class SharingCheckboxWidget extends StatefulWidget {
final Map<String, bool> sharingStatus;
const SharingCheckboxWidget(
this.sharingStatus, {
Key key,
}) : super(key: key);
@override
_SharingCheckboxWidgetState createState() => _SharingCheckboxWidgetState();
}
class _SharingCheckboxWidgetState extends State<SharingCheckboxWidget> {
Map<String, bool> _sharingStatus;
@override
void initState() {
_sharingStatus = widget.sharingStatus;
super.initState();
}
@override
Widget build(BuildContext context) {
final checkboxes = List<Widget>();
for (final user in _sharingStatus.keys) {
checkboxes.add(Row(
children: <Widget>[
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: _sharingStatus[user],
onChanged: (value) {
setState(() {
_sharingStatus[user] = value;
});
}),
Text(user),
],
));
}
return Column(children: checkboxes);
}
}

View file

@ -16,17 +16,17 @@ class SignInWidget extends StatefulWidget {
_SignInWidgetState createState() => _SignInWidgetState();
}
enum Mode { sign_up, sign_in, unknown }
class _SignInWidgetState extends State<SignInWidget> {
String _username, _password, _repeatedPassword;
@override
void initState() {
super.initState();
}
Mode mode = Mode.unknown;
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _repeatPasswordController = TextEditingController();
@override
Widget build(BuildContext context) {
if (Configuration.instance.getToken() == null) {
// Has probably not signed up
if (mode == Mode.sign_up) {
return _getSignUpWidget(context);
} else {
return _getSignInWidget(context);
@ -35,120 +35,121 @@ class _SignInWidgetState extends State<SignInWidget> {
Widget _getSignUpWidget(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
child: Text("Create an account to get started"),
),
TextFormField(
decoration: InputDecoration(
hintText: 'username',
contentPadding: EdgeInsets.all(20),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
child: Text("Create an account to get started"),
),
autofocus: true,
autocorrect: false,
onChanged: (value) {
setState(() {
_username = value;
});
},
),
TextFormField(
decoration: InputDecoration(
hintText: 'password',
contentPadding: EdgeInsets.all(20),
TextFormField(
decoration: InputDecoration(
hintText: 'username',
contentPadding: EdgeInsets.all(20),
),
controller: _usernameController,
autofocus: true,
autocorrect: false,
),
autocorrect: false,
obscureText: true,
onChanged: (value) {
setState(() {
_password = value;
});
},
),
TextFormField(
decoration: InputDecoration(
hintText: 'repeat password',
contentPadding: EdgeInsets.all(20),
TextFormField(
decoration: InputDecoration(
hintText: 'password',
contentPadding: EdgeInsets.all(20),
),
autocorrect: false,
obscureText: true,
controller: _passwordController,
),
autocorrect: false,
obscureText: true,
onChanged: (value) {
setState(() {
_repeatedPassword = value;
});
},
),
CupertinoButton(
child: Text("Sign Up"),
onPressed: () async {
if (_password != _repeatedPassword) {
_showPasswordMismatchDialog();
} else {
try {
final userCreated = await UserAuthenticator.instance
.create(_username, _password);
if (userCreated) {
Navigator.of(context).pop();
} else {
_showGenericErrorDialog();
TextFormField(
decoration: InputDecoration(
hintText: 'repeat password',
contentPadding: EdgeInsets.all(20),
),
autocorrect: false,
obscureText: true,
controller: _repeatPasswordController,
),
CupertinoButton(
child: Text("Sign Up"),
onPressed: () async {
if (_passwordController.text != _repeatPasswordController.text) {
_showPasswordMismatchDialog();
} else {
try {
final userCreated = await UserAuthenticator.instance.create(
_usernameController.text, _passwordController.text);
if (userCreated) {
Navigator.of(context).pop();
} else {
_showGenericErrorDialog();
}
} catch (e) {
_showGenericErrorDialog(error: e);
}
} catch (e) {
_showGenericErrorDialog(error: e);
}
}
},
),
],
},
),
CupertinoButton(
child: Text("Have an account?"),
onPressed: () {
setState(() {
mode = Mode.sign_in;
});
},
),
],
),
));
}
Widget _getSignInWidget(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
TextFormField(
initialValue: Configuration.instance.getUsername(),
decoration: InputDecoration(
hintText: 'username',
contentPadding: EdgeInsets.all(20),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
// initialValue: Configuration.instance.getUsername(),
decoration: InputDecoration(
hintText: 'username',
contentPadding: EdgeInsets.all(20),
),
autofocus: true,
autocorrect: false,
controller: _usernameController,
),
autofocus: true,
autocorrect: false,
onChanged: (value) {
setState(() {
_username = value;
});
},
),
TextFormField(
initialValue: Configuration.instance.getPassword(),
decoration: InputDecoration(
hintText: 'password',
contentPadding: EdgeInsets.all(20),
TextFormField(
// initialValue: Configuration.instance.getPassword(),
decoration: InputDecoration(
hintText: 'password',
contentPadding: EdgeInsets.all(20),
),
autocorrect: false,
obscureText: true,
controller: _passwordController,
),
autocorrect: false,
obscureText: true,
onChanged: (value) {
setState(() {
_password = value;
});
},
),
CupertinoButton(
child: Text("Sign In"),
onPressed: () async {
final loggedIn =
await UserAuthenticator.instance.login(_username, _password);
if (loggedIn) {
Navigator.of(context).pop();
} else {
_showAuthenticationFailedErrorDialog();
}
},
),
],
CupertinoButton(
child: Text("Sign In"),
onPressed: () async {
final loggedIn = await UserAuthenticator.instance
.login(_usernameController.text, _passwordController.text);
if (loggedIn) {
Navigator.of(context).pop();
} else {
_showAuthenticationFailedErrorDialog();
}
},
),
CupertinoButton(
child: Text("Don't have an account?"),
onPressed: () {
setState(() {
mode = Mode.sign_up;
});
},
),
],
),
));
}

View file

@ -16,10 +16,11 @@ class UserAuthenticator {
Future<bool> login(String username, String password) {
return _dio.post(
"http://" +
Configuration.instance.getEndpoint() +
":8080/users/authenticate",
data: {"username": username, "password": password}).then((response) {
Configuration.instance.getHttpEndpoint() + "/users/authenticate",
data: {
"username": username,
"password": password,
}).then((response) {
if (response.statusCode == 200 && response.data != null) {
Configuration.instance.setUsername(username);
Configuration.instance.setPassword(password);
@ -36,9 +37,11 @@ class UserAuthenticator {
}
Future<bool> create(String username, String password) {
return _dio.post(
"http://" + Configuration.instance.getEndpoint() + ":8080/users",
data: {"username": username, "password": password}).then((response) {
return _dio
.post(Configuration.instance.getHttpEndpoint() + "/users", data: {
"username": username,
"password": password,
}).then((response) {
if (response.statusCode == 200 && response.data != null) {
Configuration.instance.setUsername(username);
Configuration.instance.setPassword(password);

View file

@ -213,7 +213,7 @@ packages:
source: hosted
version: "1.0.0"
logging:
dependency: transitive
dependency: "direct main"
description:
name: logging
url: "https://pub.dartlang.org"

View file

@ -46,6 +46,7 @@ dependencies:
archive: ^2.0.11
flutter_email_sender: ^3.0.1
like_button: ^0.2.0
logging: ^0.11.4
dev_dependencies:
flutter_test: