ente/lib/ui/sharing/manage_links_widget.dart

611 lines
22 KiB
Dart
Raw Normal View History

// @dart=2.9
import 'dart:convert';
2022-02-21 02:13:10 +00:00
import 'package:collection/collection.dart';
2022-02-21 02:13:10 +00:00
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/ente_theme_data.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/colors.dart';
2022-11-20 10:14:45 +00:00
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
2022-02-26 12:25:15 +00:00
import 'package:photos/ui/common/dialogs.dart';
2022-11-20 10:14:45 +00:00
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/divider_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/components/menu_section_description_widget.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';
2022-02-21 02:13:10 +00:00
import 'package:tuple/tuple.dart';
class ManageSharedLinkWidget extends StatefulWidget {
final Collection collection;
const ManageSharedLinkWidget({Key key, this.collection}) : super(key: key);
2022-02-21 02:13:10 +00:00
@override
2022-07-03 09:45:00 +00:00
State<ManageSharedLinkWidget> createState() => _ManageSharedLinkWidgetState();
2022-02-21 02:13:10 +00:00
}
class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
// index, title, milliseconds in future post which link should expire (when >0)
final List<Tuple3<int, String, int>> _expiryOptions = [
2022-07-04 06:02:17 +00:00
const Tuple3(0, "Never", 0),
Tuple3(1, "After 1 hour", const Duration(hours: 1).inMicroseconds),
Tuple3(2, "After 1 day", const Duration(days: 1).inMicroseconds),
Tuple3(3, "After 1 week", const Duration(days: 7).inMicroseconds),
2022-02-21 02:13:10 +00:00
// todo: make this time calculation perfect
2022-07-04 06:02:17 +00:00
Tuple3(4, "After 1 month", const Duration(days: 30).inMicroseconds),
Tuple3(5, "After 1 year", const Duration(days: 365).inMicroseconds),
const Tuple3(6, "Custom", -1),
2022-02-21 02:13:10 +00:00
];
Tuple3<int, String, int> _selectedExpiry;
int _selectedDeviceLimitIndex = 0;
2022-12-15 10:02:46 +00:00
final CollectionActions sharingActions =
CollectionActions(CollectionsService.instance);
2022-02-21 02:13:10 +00:00
2022-02-22 11:29:16 +00:00
@override
void initState() {
_selectedExpiry = _expiryOptions.first;
2022-02-22 11:29:16 +00:00
super.initState();
}
2022-02-21 02:13:10 +00:00
@override
Widget build(BuildContext context) {
2022-11-20 10:14:45 +00:00
final enteColorScheme = getEnteColorScheme(context);
final PublicURL url = widget.collection?.publicURLs?.firstOrNull;
2022-02-21 02:13:10 +00:00
return Scaffold(
2022-05-16 20:38:11 +00:00
backgroundColor: Theme.of(context).backgroundColor,
2022-02-21 02:13:10 +00:00
appBar: AppBar(
elevation: 0,
2022-07-04 06:02:17 +00:00
title: const Text(
2022-05-16 20:38:11 +00:00
"Manage link",
2022-02-21 02:13:10 +00:00
),
),
body: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Padding(
2022-02-26 13:00:38 +00:00
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
2022-02-21 02:13:10 +00:00
child: Column(
2022-11-20 10:14:45 +00:00
crossAxisAlignment: CrossAxisAlignment.start,
2022-02-21 02:13:10 +00:00
children: [
2022-12-12 07:25:46 +00:00
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Allow adding photos",
),
alignCaptionedTextToLeft: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: widget.collection.publicURLs?.firstOrNull
?.enableCollect ??
2022-12-12 07:25:46 +00:00
false,
onChanged: (value) async {
await _updateUrlSettings(
context,
{'enableCollect': value},
);
setState(() {});
},
),
),
const MenuSectionDescriptionWidget(
content:
"Allow people with the link to also add photos to the shared "
2022-12-12 07:25:46 +00:00
"album.",
),
2022-12-12 07:32:00 +00:00
const SizedBox(height: 24),
2022-11-20 10:14:45 +00:00
MenuItemWidget(
alignCaptionedTextToLeft: true,
captionedTextWidget: CaptionedTextWidget(
title: "Link expiry",
2022-11-21 09:28:17 +00:00
subTitle: (url.hasExpiry
? (url.isExpired ? "Expired" : "Enabled")
: "Never"),
subTitleColor: url.isExpired ? warning500 : null,
2022-11-20 10:14:45 +00:00
),
trailingIcon: Icons.chevron_right,
menuItemColor: enteColorScheme.fillFaint,
2022-02-21 02:13:10 +00:00
onTap: () async {
2022-02-22 11:24:59 +00:00
await showPicker();
2022-02-21 02:13:10 +00:00
},
),
2022-11-21 09:28:17 +00:00
url.hasExpiry
2022-11-20 10:14:45 +00:00
? MenuSectionDescriptionWidget(
2022-11-21 09:28:17 +00:00
content: url.isExpired
2022-11-21 09:18:05 +00:00
? "This link has expired. Please select a new expiry time or disable link expiry."
: 'Link will expire on '
2022-11-21 09:28:17 +00:00
'${getFormattedTime(DateTime.fromMicrosecondsSinceEpoch(url.validTill))}',
2022-11-20 10:14:45 +00:00
)
: const SizedBox.shrink(),
const Padding(padding: EdgeInsets.only(top: 24)),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: "Device limit",
subTitle: widget.collection.publicURLs.first.deviceLimit
.toString(),
2022-02-26 13:01:47 +00:00
),
2022-11-20 10:14:45 +00:00
trailingIcon: Icons.chevron_right,
menuItemColor: enteColorScheme.fillFaint,
alignCaptionedTextToLeft: true,
isBottomBorderRadiusRemoved: true,
onTap: () async {
await _showDeviceLimitPicker();
},
2022-02-26 13:01:47 +00:00
),
2022-11-20 10:14:45 +00:00
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).blurStrokeFaint,
2022-02-21 02:13:10 +00:00
),
2022-11-20 10:14:45 +00:00
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Allow downloads",
),
alignCaptionedTextToLeft: true,
isBottomBorderRadiusRemoved: true,
isTopBorderRadiusRemoved: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: widget.collection.publicURLs?.firstOrNull
?.enableDownload ??
true,
2022-11-20 10:14:45 +00:00
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).colorScheme.greenText,
secondActionColor: Theme.of(context)
.colorScheme
.inverseBackgroundColor,
);
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(() {});
},
),
),
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).blurStrokeFaint,
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Password lock",
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: widget.collection.publicURLs?.firstOrNull
?.passwordEnabled ??
2022-11-20 10:14:45 +00:00
false,
onChanged: (enablePassword) async {
if (enablePassword) {
final inputResult =
await _displayLinkPasswordInput(context);
if (inputResult != null &&
inputResult == 'ok' &&
_textFieldController.text.trim().isNotEmpty) {
final propToUpdate = await _getEncryptedPassword(
_textFieldController.text,
2022-06-11 08:23:52 +00:00
);
2022-11-20 10:14:45 +00:00
await _updateUrlSettings(context, propToUpdate);
}
} else {
await _updateUrlSettings(
context,
{'disablePassword': true},
);
}
setState(() {});
},
2022-02-21 02:13:10 +00:00
),
),
const SizedBox(
height: 24,
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Remove link",
textColor: warning500,
2022-11-21 02:11:04 +00:00
makeTextBold: true,
),
leadingIcon: Icons.remove_circle_outline,
leadingIconColor: warning500,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
final bool result = await sharingActions.publicLinkToggle(
context,
widget.collection,
false,
);
if (result && mounted) {
Navigator.of(context).pop();
// setState(() => {});
}
},
),
2022-02-21 02:13:10 +00:00
],
),
),
],
),
),
);
}
Future<void> showPicker() async {
Widget getOptionText(String text) {
return Text(text, style: Theme.of(context).textTheme.subtitle1);
2022-02-21 02:13:10 +00:00
}
2022-02-22 11:24:59 +00:00
return showCupertinoModalPopup(
2022-02-21 02:13:10 +00:00
context: context,
builder: (context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.cupertinoPickerTopColor,
2022-07-04 06:02:17 +00:00
border: const Border(
2022-02-21 02:13:10 +00:00
bottom: BorderSide(
color: Color(0xff999999),
width: 0.0,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CupertinoButton(
onPressed: () {
2022-02-22 11:24:59 +00:00
Navigator.of(context).pop('cancel');
2022-02-21 02:13:10 +00:00
},
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 5.0,
),
child: Text(
2022-07-03 09:45:00 +00:00
'Cancel',
style: Theme.of(context).textTheme.subtitle1,
),
2022-07-03 09:45:00 +00:00
),
CupertinoButton(
2022-02-21 02:13:10 +00:00
onPressed: () async {
int newValidTill = -1;
2022-11-20 10:14:45 +00:00
final int expireAfterInMicroseconds =
_selectedExpiry.item3;
// need to manually select time
if (expireAfterInMicroseconds < 0) {
2022-08-29 14:43:31 +00:00
final timeInMicrosecondsFromEpoch =
2022-07-03 09:49:33 +00:00
await _showDateTimePicker();
if (timeInMicrosecondsFromEpoch != null) {
newValidTill = timeInMicrosecondsFromEpoch;
}
} else if (expireAfterInMicroseconds == 0) {
// no expiry
newValidTill = 0;
2022-02-21 02:13:10 +00:00
} else {
2022-07-03 09:49:33 +00:00
newValidTill = DateTime.now().microsecondsSinceEpoch +
expireAfterInMicroseconds;
2022-02-21 02:13:10 +00:00
}
if (newValidTill >= 0) {
await _updateUrlSettings(
2022-06-11 08:23:52 +00:00
context,
{'validTill': newValidTill},
);
setState(() {});
}
Navigator.of(context).pop('');
2022-02-21 02:13:10 +00:00
},
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 2.0,
),
2022-07-03 09:45:00 +00:00
child: Text(
'Confirm',
style: Theme.of(context).textTheme.subtitle1,
),
2022-02-21 02:13:10 +00:00
)
],
),
),
Container(
height: 220.0,
2022-07-04 06:02:17 +00:00
color: const Color(0xfff7f7f7),
2022-02-21 02:13:10 +00:00
child: CupertinoPicker(
2022-07-03 09:49:33 +00:00
backgroundColor:
Theme.of(context).backgroundColor.withOpacity(0.95),
2022-02-21 02:13:10 +00:00
onSelectedItemChanged: (value) {
2022-08-29 14:43:31 +00:00
final firstWhere = _expiryOptions
2022-07-03 09:49:33 +00:00
.firstWhere((element) => element.item1 == value);
2022-02-21 02:13:10 +00:00
setState(() {
_selectedExpiry = firstWhere;
});
},
magnification: 1.3,
useMagnifier: true,
itemExtent: 25,
diameterRatio: 1,
2022-07-03 09:49:33 +00:00
children:
_expiryOptions.map((e) => getOptionText(e.item2)).toList(),
2022-02-21 02:13:10 +00:00
),
)
],
);
},
);
}
// _showDateTimePicker return null if user doesn't select date-time
Future<int> _showDateTimePicker() async {
2022-02-21 02:13:10 +00:00
final dateResult = await DatePicker.showDatePicker(
context,
minTime: DateTime.now(),
2022-02-21 02:13:10 +00:00
currentTime: DateTime.now(),
locale: LocaleType.en,
2022-06-06 10:32:10 +00:00
theme: Theme.of(context).colorScheme.dateTimePickertheme,
2022-02-21 02:13:10 +00:00
);
if (dateResult == null) {
return null;
}
final dateWithTimeResult = await DatePicker.showTime12hPicker(
context,
showTitleActions: true,
currentTime: dateResult,
locale: LocaleType.en,
2022-06-06 10:32:10 +00:00
theme: Theme.of(context).colorScheme.dateTimePickertheme,
2022-02-21 02:13:10 +00:00
);
if (dateWithTimeResult == null) {
return null;
} else {
return dateWithTimeResult.microsecondsSinceEpoch;
}
}
final TextEditingController _textFieldController = TextEditingController();
Future<String> _displayLinkPasswordInput(BuildContext context) async {
_textFieldController.clear();
return showDialog<String>(
2022-06-11 08:23:52 +00:00
context: context,
builder: (context) {
2022-07-12 06:30:02 +00:00
bool passwordVisible = false;
2022-06-11 08:23:52 +00:00
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
2022-07-04 06:02:17 +00:00
title: const Text('Enter password'),
content: TextFormField(
autofillHints: const [AutofillHints.newPassword],
decoration: InputDecoration(
2022-05-30 01:21:37 +00:00
hintText: "Password",
2022-07-04 06:02:17 +00:00
contentPadding: const EdgeInsets.all(12),
suffixIcon: IconButton(
icon: Icon(
2022-07-12 06:30:02 +00:00
passwordVisible ? Icons.visibility : Icons.visibility_off,
color: Colors.white.withOpacity(0.5),
size: 20,
),
onPressed: () {
2022-07-12 06:30:02 +00:00
passwordVisible = !passwordVisible;
setState(() {});
},
),
),
2022-07-12 06:30:02 +00:00
obscureText: !passwordVisible,
controller: _textFieldController,
2022-02-26 13:07:28 +00:00
autofocus: true,
autocorrect: false,
keyboardType: TextInputType.visiblePassword,
onChanged: (_) {
setState(() {});
},
),
actions: <Widget>[
TextButton(
2022-06-11 08:23:52 +00:00
child: Text(
'Cancel',
style: Theme.of(context).textTheme.subtitle2,
),
onPressed: () {
Navigator.pop(context, 'cancel');
},
),
TextButton(
2022-07-03 09:49:33 +00:00
child:
Text('Ok', style: Theme.of(context).textTheme.subtitle2),
onPressed: () {
if (_textFieldController.text.trim().isEmpty) {
return;
}
Navigator.pop(context, 'ok');
},
),
],
);
2022-06-11 08:23:52 +00:00
},
);
},
);
}
Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
2022-06-11 08:23:52 +00:00
assert(
Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
"mismatch in expected default pw hashing algo",
);
2022-08-29 14:43:31 +00:00
final int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final result = await CryptoUtil.deriveKey(
2022-06-11 08:23:52 +00:00
utf8.encode(pass),
kekSalt,
memLimit,
opsLimit,
);
return {
'passHash': Sodium.bin2base64(result),
'nonce': Sodium.bin2base64(kekSalt),
'memLimit': memLimit,
'opsLimit': opsLimit,
};
}
Future<void> _updateUrlSettings(
2022-06-11 08:23:52 +00:00
BuildContext context,
Map<String, dynamic> prop,
) async {
2022-05-17 11:38:21 +00:00
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();
try {
await CollectionsService.instance.updateShareUrl(widget.collection, prop);
await dialog.hide();
2022-06-10 14:29:56 +00:00
showToast(context, "Album updated");
} catch (e) {
await dialog.hide();
await showGenericErrorDialog(context);
}
}
2022-02-26 11:51:52 +00:00
Text _getLinkExpiryTimeWidget() {
final int validTill =
widget.collection.publicURLs?.firstOrNull?.validTill ?? 0;
if (validTill == 0) {
2022-07-04 06:02:17 +00:00
return const Text(
2022-05-30 01:21:37 +00:00
'Never',
2022-02-26 11:51:52 +00:00
style: TextStyle(
color: Colors.grey,
),
);
}
if (validTill < DateTime.now().microsecondsSinceEpoch) {
2022-02-26 11:51:52 +00:00
return Text(
2022-05-30 01:21:37 +00:00
'Expired',
2022-02-26 11:51:52 +00:00
style: TextStyle(
color: Colors.orange[300],
),
);
}
2022-02-26 11:51:52 +00:00
return Text(
getFormattedTime(DateTime.fromMicrosecondsSinceEpoch(validTill)),
2022-07-04 06:02:17 +00:00
style: const TextStyle(
2022-02-26 11:51:52 +00:00
color: Colors.grey,
),
);
}
Future<void> _showDeviceLimitPicker() async {
2022-08-29 14:43:31 +00:00
final List<Text> options = [];
for (int i = 50; i > 0; i--) {
options.add(
2022-06-11 08:23:52 +00:00
Text(i.toString(), style: Theme.of(context).textTheme.subtitle1),
);
}
return showCupertinoModalPopup(
context: context,
builder: (context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.cupertinoPickerTopColor,
2022-07-04 06:02:17 +00:00
border: const Border(
bottom: BorderSide(
color: Color(0xff999999),
width: 0.0,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CupertinoButton(
onPressed: () {
Navigator.of(context).pop('cancel');
},
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 5.0,
),
child: Text(
2022-07-03 09:45:00 +00:00
'Cancel',
style: Theme.of(context).textTheme.subtitle1,
),
2022-07-03 09:45:00 +00:00
),
CupertinoButton(
onPressed: () async {
await _updateUrlSettings(context, {
'deviceLimit': int.tryParse(
2022-06-11 08:23:52 +00:00
options[_selectedDeviceLimitIndex].data,
),
});
setState(() {});
Navigator.of(context).pop('');
},
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 2.0,
),
2022-07-03 09:45:00 +00:00
child: Text(
'Confirm',
style: Theme.of(context).textTheme.subtitle1,
),
)
],
),
),
Container(
height: 220.0,
2022-07-04 06:02:17 +00:00
color: const Color(0xfff7f7f7),
child: CupertinoPicker(
2022-07-03 09:49:33 +00:00
backgroundColor:
Theme.of(context).backgroundColor.withOpacity(0.95),
onSelectedItemChanged: (value) {
_selectedDeviceLimitIndex = value;
},
magnification: 1.3,
useMagnifier: true,
itemExtent: 25,
diameterRatio: 1,
2022-07-03 09:45:00 +00:00
children: options,
),
)
],
);
},
);
}
2022-02-21 02:13:10 +00:00
}