Merge pull request #193 from ente-io/link_sharing_v2
Support for setting link expiry, download button. & password
This commit is contained in:
commit
6907dd2d3a
9 changed files with 677 additions and 53 deletions
|
@ -317,18 +317,23 @@ class PublicURL {
|
|||
String url;
|
||||
int deviceLimit;
|
||||
int validTill;
|
||||
bool enableDownload = true;
|
||||
bool passwordEnabled = false;
|
||||
|
||||
PublicURL({
|
||||
this.url,
|
||||
this.deviceLimit,
|
||||
this.validTill,
|
||||
});
|
||||
PublicURL(
|
||||
{this.url,
|
||||
this.deviceLimit,
|
||||
this.validTill,
|
||||
this.enableDownload,
|
||||
this.passwordEnabled});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'url': url,
|
||||
'deviceLimit': deviceLimit,
|
||||
'validTill': validTill,
|
||||
'enableDownload': enableDownload,
|
||||
'passwordEnabled': passwordEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -338,7 +343,9 @@ class PublicURL {
|
|||
return PublicURL(
|
||||
url: map['url'],
|
||||
deviceLimit: map['deviceLimit'],
|
||||
validTill: map['validTill'],
|
||||
validTill: map['validTill'] ?? 0,
|
||||
enableDownload: map['enableDownload'] ?? true,
|
||||
passwordEnabled: map['passwordEnabled'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -349,7 +356,7 @@ class PublicURL {
|
|||
|
||||
@override
|
||||
String toString() =>
|
||||
'PublicUrl( url: $url, deviceLimit: $deviceLimit, validTill: $validTill)';
|
||||
'PublicUrl( url: $url, deviceLimit: $deviceLimit, validTill: $validTill, , enableDownload: $enableDownload, , passwordEnabled: $passwordEnabled)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) {
|
||||
|
@ -358,9 +365,16 @@ class PublicURL {
|
|||
return o is PublicURL &&
|
||||
o.deviceLimit == deviceLimit &&
|
||||
o.url == url &&
|
||||
o.validTill == validTill;
|
||||
o.validTill == validTill &&
|
||||
o.enableDownload == enableDownload &&
|
||||
o.passwordEnabled == passwordEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => deviceLimit.hashCode ^ url.hashCode ^ validTill.hashCode;
|
||||
int get hashCode =>
|
||||
deviceLimit.hashCode ^
|
||||
url.hashCode ^
|
||||
validTill.hashCode ^
|
||||
enableDownload.hashCode ^
|
||||
passwordEnabled.hashCode;
|
||||
}
|
||||
|
|
0
lib/models/update_share_url_request.dart
Normal file
0
lib/models/update_share_url_request.dart
Normal file
|
@ -298,6 +298,33 @@ class CollectionsService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> updateShareUrl(
|
||||
Collection collection, Map<String, dynamic> prop) async {
|
||||
prop.putIfAbsent('collectionID', () => collection.id);
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/share-url",
|
||||
data: json.encode(prop),
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
);
|
||||
// remove existing url information
|
||||
collection.publicURLs?.clear();
|
||||
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));
|
||||
await _db.insert(List.from([collection]));
|
||||
_cacheCollectionAttributes(collection);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collection.id, <File>[]));
|
||||
} on DioError catch (e) {
|
||||
if (e.response.statusCode == 402) {
|
||||
throw SharingNotPermittedForFreeAccountsError();
|
||||
}
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to rename collection", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disableShareUrl(Collection collection) async {
|
||||
try {
|
||||
await _dio.delete(
|
||||
|
|
|
@ -13,7 +13,9 @@ Future<T> showChoiceDialog<T>(
|
|||
String title,
|
||||
String content, {
|
||||
String firstAction = 'ok',
|
||||
Color firstActionColor,
|
||||
String secondAction = 'cancel',
|
||||
Color secondActionColor,
|
||||
ActionType actionType = ActionType.confirm,
|
||||
}) {
|
||||
AlertDialog alert = AlertDialog(
|
||||
|
@ -34,8 +36,8 @@ Future<T> showChoiceDialog<T>(
|
|||
child: Text(
|
||||
firstAction,
|
||||
style: TextStyle(
|
||||
color:
|
||||
actionType == ActionType.critical ? Colors.red : Colors.white,
|
||||
color: firstActionColor ??
|
||||
(actionType == ActionType.critical ? Colors.red : Colors.white),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
@ -47,7 +49,7 @@ Future<T> showChoiceDialog<T>(
|
|||
child: Text(
|
||||
secondAction,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).buttonColor,
|
||||
color: secondActionColor ?? Theme.of(context).buttonColor,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
|
12
lib/ui/common/widget_theme.dart
Normal file
12
lib/ui/common/widget_theme.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
|
||||
|
||||
const kDatePickerTheme = DatePickerTheme(
|
||||
backgroundColor: Colors.black,
|
||||
itemStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
cancelStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
534
lib/ui/manage_links_widget.dart
Normal file
534
lib/ui/manage_links_widget.dart
Normal file
|
@ -0,0 +1,534 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/ui/common/dialogs.dart';
|
||||
import 'package:photos/ui/common/widget_theme.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ManageSharedLinkWidget extends StatefulWidget {
|
||||
final Collection collection;
|
||||
|
||||
ManageSharedLinkWidget({Key key, this.collection}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ManageSharedLinkWidgetState createState() => _ManageSharedLinkWidgetState();
|
||||
}
|
||||
|
||||
class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
||||
// index, title, milliseconds in future post which link should expire (when >0)
|
||||
final List<Tuple3<int, String, int>> _expiryOptions = [
|
||||
Tuple3(0, "never", 0),
|
||||
Tuple3(1, "after 1 hour", Duration(hours: 1).inMicroseconds),
|
||||
Tuple3(2, "after 1 day", Duration(days: 1).inMicroseconds),
|
||||
Tuple3(3, "after 1 week", Duration(days: 7).inMicroseconds),
|
||||
// todo: make this time calculation perfect
|
||||
Tuple3(4, "after 1 month", Duration(days: 30).inMicroseconds),
|
||||
Tuple3(5, "after 1 year", Duration(days: 365).inMicroseconds),
|
||||
Tuple3(6, "custom", -1),
|
||||
];
|
||||
|
||||
Tuple3<int, String, int> _selectedExpiry;
|
||||
int _selectedDeviceLimitIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_selectedExpiry = _expiryOptions.first;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"manage link",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
await showPicker();
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("link expiry"),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
_getLinkExpiryTimeWidget(),
|
||||
],
|
||||
),
|
||||
Icon(Icons.navigate_next),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Divider(height: 4),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
_showDeviceLimitPicker();
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("device limit"),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(
|
||||
widget.collection.publicURLs.first.deviceLimit
|
||||
.toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(Icons.navigate_next),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Divider(height: 4),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("password lock"),
|
||||
Switch.adaptive(
|
||||
value: widget.collection.publicURLs?.first
|
||||
?.passwordEnabled ??
|
||||
false,
|
||||
onChanged: (enablePassword) async {
|
||||
if (enablePassword) {
|
||||
var inputResult =
|
||||
await _displayLinkPasswordInput(context);
|
||||
if (inputResult != null &&
|
||||
inputResult == 'ok' &&
|
||||
_textFieldController.text.trim().isNotEmpty) {
|
||||
var propToUpdate = await _getEncryptedPassword(
|
||||
_textFieldController.text);
|
||||
await _updateUrlSettings(context, propToUpdate);
|
||||
}
|
||||
} else {
|
||||
await _updateUrlSettings(
|
||||
context, {'disablePassword': true});
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Divider(height: 4),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("file download"),
|
||||
Switch.adaptive(
|
||||
value: widget.collection.publicURLs?.first
|
||||
?.enableDownload ??
|
||||
true,
|
||||
onChanged: (value) async {
|
||||
if (!value) {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
'disable downloads',
|
||||
'are you sure that you want to disable the download button for files?',
|
||||
firstAction: 'no',
|
||||
secondAction: 'yes',
|
||||
firstActionColor: Theme.of(context).buttonColor,
|
||||
secondActionColor: Colors.white,
|
||||
);
|
||||
if (choice != DialogUserChoice.secondChoice) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await _updateUrlSettings(
|
||||
context, {'enableDownload': value});
|
||||
if (!value) {
|
||||
showErrorDialog(context, "please note",
|
||||
"viewers can still take screenshots or save a copy of your photos using external tools");
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showPicker() async {
|
||||
Widget getOptionText(String text) {
|
||||
return Text(text, style: TextStyle(color: Colors.white));
|
||||
}
|
||||
|
||||
return showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xff999999),
|
||||
width: 0.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
CupertinoButton(
|
||||
child: Text('cancel',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
)),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('cancel');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 5.0,
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
child: Text('confirm',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
)),
|
||||
onPressed: () async {
|
||||
int newValidTill = -1;
|
||||
int expireAfterInMicroseconds = _selectedExpiry.item3;
|
||||
// need to manually select time
|
||||
if (expireAfterInMicroseconds < 0) {
|
||||
var timeInMicrosecondsFromEpoch =
|
||||
await _showDateTimePicker();
|
||||
if (timeInMicrosecondsFromEpoch != null) {
|
||||
newValidTill = timeInMicrosecondsFromEpoch;
|
||||
}
|
||||
} else if (expireAfterInMicroseconds == 0) {
|
||||
// no expiry
|
||||
newValidTill = 0;
|
||||
} else {
|
||||
newValidTill = DateTime.now().microsecondsSinceEpoch +
|
||||
expireAfterInMicroseconds;
|
||||
}
|
||||
if (newValidTill >= 0) {
|
||||
await _updateUrlSettings(
|
||||
context, {'validTill': newValidTill});
|
||||
setState(() {});
|
||||
}
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 2.0,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 220.0,
|
||||
color: Color(0xfff7f7f7),
|
||||
child: CupertinoPicker(
|
||||
backgroundColor: Colors.black.withOpacity(0.95),
|
||||
children:
|
||||
_expiryOptions.map((e) => getOptionText(e.item2)).toList(),
|
||||
onSelectedItemChanged: (value) {
|
||||
var firstWhere = _expiryOptions
|
||||
.firstWhere((element) => element.item1 == value);
|
||||
setState(() {
|
||||
_selectedExpiry = firstWhere;
|
||||
});
|
||||
},
|
||||
magnification: 1.3,
|
||||
useMagnifier: true,
|
||||
itemExtent: 25,
|
||||
diameterRatio: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// _showDateTimePicker return null if user doesn't select date-time
|
||||
Future<int> _showDateTimePicker() async {
|
||||
final dateResult = await DatePicker.showDatePicker(
|
||||
context,
|
||||
minTime: DateTime.now(),
|
||||
currentTime: DateTime.now(),
|
||||
locale: LocaleType.en,
|
||||
theme: kDatePickerTheme,
|
||||
);
|
||||
if (dateResult == null) {
|
||||
return null;
|
||||
}
|
||||
final dateWithTimeResult = await DatePicker.showTime12hPicker(
|
||||
context,
|
||||
showTitleActions: true,
|
||||
currentTime: dateResult,
|
||||
locale: LocaleType.en,
|
||||
theme: kDatePickerTheme,
|
||||
);
|
||||
if (dateWithTimeResult == null) {
|
||||
return null;
|
||||
} else {
|
||||
return dateWithTimeResult.microsecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
|
||||
final TextEditingController _textFieldController = TextEditingController();
|
||||
|
||||
Future<String> _displayLinkPasswordInput(BuildContext context) async {
|
||||
_textFieldController.clear();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
bool _passwordVisible = false;
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text('enter password'),
|
||||
content: TextFormField(
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: InputDecoration(
|
||||
hintText: "password",
|
||||
contentPadding: EdgeInsets.all(12),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
obscureText: !_passwordVisible,
|
||||
controller: _textFieldController,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(
|
||||
'cancel',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'cancel');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'ok',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_textFieldController.text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
Navigator.pop(context, 'ok');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
|
||||
assert(Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
|
||||
"mismatch in expected default pw hashing algo");
|
||||
int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
|
||||
int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
|
||||
final kekSalt = CryptoUtil.getSaltToDeriveKey();
|
||||
final result = await CryptoUtil.deriveKey(
|
||||
utf8.encode(pass), kekSalt, memLimit, opsLimit);
|
||||
return {
|
||||
'passHash': Sodium.bin2base64(result),
|
||||
'nonce': Sodium.bin2base64(kekSalt),
|
||||
'memLimit': memLimit,
|
||||
'opsLimit': opsLimit,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _updateUrlSettings(
|
||||
BuildContext context, Map<String, dynamic> prop) async {
|
||||
final dialog = createProgressDialog(context, "please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
await CollectionsService.instance.updateShareUrl(widget.collection, prop);
|
||||
await dialog.hide();
|
||||
showToast("album updated");
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
await showGenericErrorDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Text _getLinkExpiryTimeWidget() {
|
||||
int validTill = widget.collection.publicURLs?.first?.validTill ?? 0;
|
||||
if (validTill == 0) {
|
||||
return Text(
|
||||
'never',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (validTill < DateTime.now().microsecondsSinceEpoch) {
|
||||
return Text(
|
||||
'expired',
|
||||
style: TextStyle(
|
||||
color: Colors.orange[300],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
getFormattedTime(DateTime.fromMicrosecondsSinceEpoch(validTill)),
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDeviceLimitPicker() async {
|
||||
List<Text> options = [];
|
||||
for (int i = 50; i > 0; i--) {
|
||||
options.add(Text(i.toString(), style: TextStyle(color: Colors.white)));
|
||||
}
|
||||
return showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xff999999),
|
||||
width: 0.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
CupertinoButton(
|
||||
child: Text('cancel',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
)),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('cancel');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 5.0,
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
child: Text('confirm',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
)),
|
||||
onPressed: () async {
|
||||
await _updateUrlSettings(context, {
|
||||
'deviceLimit': int.tryParse(
|
||||
options[_selectedDeviceLimitIndex].data),
|
||||
});
|
||||
setState(() {});
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 2.0,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 220.0,
|
||||
color: Color(0xfff7f7f7),
|
||||
child: CupertinoPicker(
|
||||
backgroundColor: Colors.black.withOpacity(0.95),
|
||||
children: options,
|
||||
onSelectedItemChanged: (value) {
|
||||
_selectedDeviceLimitIndex = value;
|
||||
},
|
||||
magnification: 1.3,
|
||||
useMagnifier: true,
|
||||
itemExtent: 25,
|
||||
diameterRatio: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,9 +16,11 @@ import 'package:photos/services/user_service.dart';
|
|||
import 'package:photos/ui/common/dialogs.dart';
|
||||
import 'package:photos/ui/common_elements.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/manage_links_widget.dart';
|
||||
import 'package:photos/ui/payment/subscription.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/email_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import 'package:photos/utils/share_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
|
@ -138,7 +140,10 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
),
|
||||
]);
|
||||
if (widget.collection.publicURLs?.isNotEmpty ?? false) {
|
||||
children.add(_getShareableUrlWidget());
|
||||
children.add(Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
));
|
||||
children.add(_getShareableUrlWidget(context));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,6 +160,7 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
],
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.fromLTRB(24, 24, 24, 4),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -209,49 +215,51 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _getShareableUrlWidget() {
|
||||
Widget _getShareableUrlWidget(BuildContext parentContext) {
|
||||
String collectionKey = Base58Encode(
|
||||
CollectionsService.instance.getCollectionKey(widget.collection.id));
|
||||
String url = "${widget.collection.publicURLs.first.url}#$collectionKey";
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
showToast("link copied to clipboard");
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
url,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
color: Colors.white.withOpacity(0.68),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
showToast("link copied to clipboard");
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
url,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
color: Colors.white.withOpacity(0.68),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
Icon(
|
||||
Icons.copy,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
color: Colors.white.withOpacity(0.02),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
Icon(
|
||||
Icons.copy,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
color: Colors.white.withOpacity(0.02),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
TextButton(
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
TextButton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
@ -270,12 +278,31 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
shareText(url);
|
||||
// _shareRecoveryKey(recoveryKey);
|
||||
},
|
||||
),
|
||||
]),
|
||||
onPressed: () {
|
||||
shareText(url);
|
||||
},
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"manage link",
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
routeToPage(
|
||||
parentContext,
|
||||
ManageSharedLinkWidget(collection: widget.collection),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1152,6 +1152,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tuple
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -11,7 +11,7 @@ description: ente photos application
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.5.5+284
|
||||
version: 0.5.6+284
|
||||
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
|
@ -96,6 +96,7 @@ dependencies:
|
|||
path: thirdparty/super_logging
|
||||
syncfusion_flutter_core: ^19.2.49
|
||||
syncfusion_flutter_sliders: ^19.2.49
|
||||
tuple: ^2.0.0
|
||||
uni_links: ^0.5.1
|
||||
url_launcher: ^6.0.3
|
||||
uuid: ^3.0.4
|
||||
|
|
Loading…
Add table
Reference in a new issue