Support for enabling/disabling public urls
This commit is contained in:
parent
acdae0e186
commit
ff61b7113a
|
@ -26,6 +26,7 @@ class CollectionsDB {
|
|||
static final columnPathDecryptionNonce = 'path_decryption_nonce';
|
||||
static final columnVersion = 'version';
|
||||
static final columnSharees = 'sharees';
|
||||
static final columnPublicURLs = 'public_urls';
|
||||
static final columnUpdationTime = 'updation_time';
|
||||
static final columnIsDeleted = 'is_deleted';
|
||||
|
||||
|
@ -35,6 +36,7 @@ class CollectionsDB {
|
|||
...addEncryptedName(),
|
||||
...addVersion(),
|
||||
...addIsDeleted(),
|
||||
...addPublicURLs(),
|
||||
];
|
||||
|
||||
final dbConfig = MigrationConfig(
|
||||
|
@ -127,6 +129,15 @@ class CollectionsDB {
|
|||
];
|
||||
}
|
||||
|
||||
static List<String> addPublicURLs() {
|
||||
return [
|
||||
'''
|
||||
ALTER TABLE $table
|
||||
ADD COLUMN $columnPublicURLs TEXT;
|
||||
'''
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<dynamic>> insert(List<Collection> collections) async {
|
||||
final db = await instance.database;
|
||||
var batch = db.batch();
|
||||
|
@ -185,6 +196,8 @@ class CollectionsDB {
|
|||
row[columnVersion] = collection.attributes.version;
|
||||
row[columnSharees] =
|
||||
json.encode(collection.sharees?.map((x) => x?.toMap())?.toList());
|
||||
row[columnPublicURLs] =
|
||||
json.encode(collection.publicURLs?.map((x) => x?.toMap())?.toList());
|
||||
row[columnUpdationTime] = collection.updationTime;
|
||||
if (collection.isDeleted ?? false) {
|
||||
row[columnIsDeleted] = _sqlBoolTrue;
|
||||
|
@ -211,6 +224,8 @@ class CollectionsDB {
|
|||
),
|
||||
List<User>.from((json.decode(row[columnSharees]) as List)
|
||||
.map((x) => User.fromMap(x))),
|
||||
List<PublicURL>.from((json.decode(row[columnPublicURLs]) as List)
|
||||
.map((x) => PublicURL.fromMap(x))),
|
||||
int.parse(row[columnUpdationTime]),
|
||||
// default to False is columnIsDeleted is not set
|
||||
isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
|
||||
|
|
|
@ -13,6 +13,7 @@ class Collection {
|
|||
final CollectionType type;
|
||||
final CollectionAttributes attributes;
|
||||
final List<User> sharees;
|
||||
final List<PublicURL> publicURLs;
|
||||
final int updationTime;
|
||||
final bool isDeleted;
|
||||
|
||||
|
@ -27,6 +28,7 @@ class Collection {
|
|||
this.type,
|
||||
this.attributes,
|
||||
this.sharees,
|
||||
this.publicURLs,
|
||||
this.updationTime, {
|
||||
this.isDeleted = false,
|
||||
});
|
||||
|
@ -63,6 +65,7 @@ class Collection {
|
|||
CollectionType type,
|
||||
CollectionAttributes attributes,
|
||||
List<User> sharees,
|
||||
List<PublicURL> publicURLs,
|
||||
int updationTime,
|
||||
bool isDeleted,
|
||||
}) {
|
||||
|
@ -77,6 +80,7 @@ class Collection {
|
|||
type ?? this.type,
|
||||
attributes ?? this.attributes,
|
||||
sharees ?? this.sharees,
|
||||
publicURLs ?? this.publicURLs,
|
||||
updationTime ?? this.updationTime,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
);
|
||||
|
@ -94,6 +98,7 @@ class Collection {
|
|||
'type': typeToString(type),
|
||||
'attributes': attributes?.toMap(),
|
||||
'sharees': sharees?.map((x) => x?.toMap())?.toList(),
|
||||
'publicURLs': publicURLs?.map((x) => x?.toMap())?.toList(),
|
||||
'updationTime': updationTime,
|
||||
'isDeleted': isDeleted,
|
||||
};
|
||||
|
@ -104,6 +109,11 @@ class Collection {
|
|||
final sharees = (map['sharees'] == null || map['sharees'].length == 0)
|
||||
? <User>[]
|
||||
: List<User>.from(map['sharees'].map((x) => User.fromMap(x)));
|
||||
final publicURLs =
|
||||
(map['publicURLs'] == null || map['publicURLs'].length == 0)
|
||||
? <PublicURL>[]
|
||||
: List<PublicURL>.from(
|
||||
map['publicURLs'].map((x) => PublicURL.fromMap(x)));
|
||||
return Collection(
|
||||
map['id'],
|
||||
User.fromMap(map['owner']),
|
||||
|
@ -115,6 +125,7 @@ class Collection {
|
|||
typeFromString(map['type']),
|
||||
CollectionAttributes.fromMap(map['attributes']),
|
||||
sharees,
|
||||
publicURLs,
|
||||
map['updationTime'],
|
||||
isDeleted: map['isDeleted'] ?? false,
|
||||
);
|
||||
|
@ -127,7 +138,7 @@ class Collection {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, encryptedName: $encryptedName, nameDecryptionNonce: $nameDecryptionNonce, type: $type, attributes: $attributes, sharees: $sharees, updationTime: $updationTime, isDeleted: $isDeleted)';
|
||||
return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, encryptedName: $encryptedName, nameDecryptionNonce: $nameDecryptionNonce, type: $type, attributes: $attributes, sharees: $sharees, publicURLs: $publicURLs, updationTime: $updationTime, isDeleted: $isDeleted)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -145,6 +156,7 @@ class Collection {
|
|||
o.type == type &&
|
||||
o.attributes == attributes &&
|
||||
listEquals(o.sharees, sharees) &&
|
||||
listEquals(o.publicURLs, publicURLs) &&
|
||||
o.updationTime == updationTime &&
|
||||
o.isDeleted == isDeleted;
|
||||
}
|
||||
|
@ -161,6 +173,7 @@ class Collection {
|
|||
type.hashCode ^
|
||||
attributes.hashCode ^
|
||||
sharees.hashCode ^
|
||||
publicURLs.hashCode ^
|
||||
updationTime.hashCode ^
|
||||
isDeleted.hashCode;
|
||||
}
|
||||
|
@ -299,3 +312,55 @@ class User {
|
|||
@override
|
||||
int get hashCode => id.hashCode ^ email.hashCode ^ name.hashCode;
|
||||
}
|
||||
|
||||
class PublicURL {
|
||||
String url;
|
||||
int deviceLimit;
|
||||
int validTill;
|
||||
|
||||
PublicURL({
|
||||
this.url,
|
||||
this.deviceLimit,
|
||||
this.validTill,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'url': url,
|
||||
'deviceLimit': deviceLimit,
|
||||
'validTill': validTill,
|
||||
};
|
||||
}
|
||||
|
||||
factory PublicURL.fromMap(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
|
||||
return PublicURL(
|
||||
url: map['url'],
|
||||
deviceLimit: map['deviceLimit'],
|
||||
validTill: map['validTill'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory PublicURL.fromJson(String source) =>
|
||||
PublicURL.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'PublicUrl( url: $url, deviceLimit: $deviceLimit, validTill: $validTill)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) {
|
||||
if (identical(this, o)) return true;
|
||||
|
||||
return o is PublicURL &&
|
||||
o.deviceLimit == deviceLimit &&
|
||||
o.url == url &&
|
||||
o.validTill == validTill;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => deviceLimit.hashCode ^ url.hashCode ^ validTill.hashCode;
|
||||
}
|
||||
|
|
|
@ -273,6 +273,46 @@ class CollectionsService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> createShareUrl(Collection collection) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/share-url",
|
||||
data: {
|
||||
"collectionID": collection.id,
|
||||
},
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
);
|
||||
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));
|
||||
await _db.insert(List.from([collection]));
|
||||
_cacheCollectionAttributes(collection);
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to rename collection", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disableShareUrl(Collection collection) async {
|
||||
try {
|
||||
await _dio.delete(
|
||||
_config.getHttpEndpoint() +
|
||||
"/collections/share-url/" +
|
||||
collection.id.toString(),
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
);
|
||||
collection.publicURLs.clear();
|
||||
await _db.insert(List.from([collection]));
|
||||
_cacheCollectionAttributes(collection);
|
||||
} on DioError catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Collection>> _fetchCollections(int sinceTime) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
|
@ -320,6 +360,7 @@ class CollectionsService {
|
|||
CollectionAttributes(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
));
|
||||
return collection;
|
||||
}
|
||||
|
@ -370,6 +411,7 @@ class CollectionsService {
|
|||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
));
|
||||
return collection;
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ class FavoritesService {
|
|||
CollectionAttributes(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
));
|
||||
_cachedFavoritesCollectionID = collection.id;
|
||||
return collection.id;
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
|
@ -80,6 +85,46 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false;
|
||||
|
||||
children.addAll([
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
Divider(height: 12),
|
||||
Padding(padding: EdgeInsets.all(Platform.isIOS ? 2 : 4)),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("enable public url"),
|
||||
Switch(
|
||||
value: hasUrl,
|
||||
onChanged: (enable) async {
|
||||
final dialog = createProgressDialog(
|
||||
context, enable ? "generating url..." : "disabling url");
|
||||
try {
|
||||
await dialog.show();
|
||||
enable
|
||||
? await CollectionsService.instance
|
||||
.createShareUrl(widget.collection)
|
||||
: await CollectionsService.instance
|
||||
.disableShareUrl(widget.collection);
|
||||
} catch (e) {
|
||||
_logger.severe('failed to $enable url', e);
|
||||
} finally {
|
||||
dialog.hide();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
if (widget.collection.publicURLs?.isNotEmpty ?? false) {
|
||||
children.add(_getShareableUrlWidget());
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text("sharing"),
|
||||
content: SingleChildScrollView(
|
||||
|
@ -148,6 +193,41 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _getShareableUrlWidget() {
|
||||
String base64urlEncode = base64UrlEncode(
|
||||
CollectionsService.instance.getCollectionKey(widget.collection.id));
|
||||
String url =
|
||||
"${widget.collection.publicURLs.first.url}#collectionKey=$base64urlEncode";
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
showToast("URL is copied to clipboard");
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
url,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addEmailToCollection(
|
||||
String email, {
|
||||
String publicKey,
|
||||
|
|
Loading…
Reference in a new issue