2021-09-19 23:18:20 +02:00
|
|
|
import 'dart:async';
|
2021-10-08 20:39:53 +02:00
|
|
|
import 'dart:math';
|
2021-09-19 23:18:20 +02:00
|
|
|
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2021-09-28 08:36:00 +02:00
|
|
|
import 'package:flutter/services.dart';
|
2022-05-07 12:27:26 +02:00
|
|
|
import 'package:kiwi/kiwi.dart';
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:nc_photos/account.dart';
|
2021-07-25 07:00:38 +02:00
|
|
|
import 'package:nc_photos/app_localizations.dart';
|
2022-05-07 12:27:26 +02:00
|
|
|
import 'package:nc_photos/debug_util.dart';
|
|
|
|
import 'package:nc_photos/di_container.dart';
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:nc_photos/entity/file.dart';
|
2022-10-15 16:29:18 +02:00
|
|
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
2022-09-09 18:29:33 +02:00
|
|
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
2022-05-07 12:27:26 +02:00
|
|
|
import 'package:nc_photos/entity/local_file.dart';
|
2021-09-28 08:36:00 +02:00
|
|
|
import 'package:nc_photos/entity/share.dart';
|
|
|
|
import 'package:nc_photos/entity/share/data_source.dart';
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
2021-09-19 23:18:20 +02:00
|
|
|
import 'package:nc_photos/iterable_extension.dart';
|
2021-09-28 08:36:00 +02:00
|
|
|
import 'package:nc_photos/k.dart' as k;
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:nc_photos/mobile/share.dart';
|
|
|
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
2021-10-08 20:39:53 +02:00
|
|
|
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:nc_photos/snack_bar_manager.dart';
|
2021-10-08 20:39:53 +02:00
|
|
|
import 'package:nc_photos/use_case/copy.dart';
|
|
|
|
import 'package:nc_photos/use_case/create_dir.dart';
|
2021-09-28 08:36:00 +02:00
|
|
|
import 'package:nc_photos/use_case/create_share.dart';
|
2021-09-09 22:42:47 +02:00
|
|
|
import 'package:nc_photos/use_case/download_file.dart';
|
2022-09-09 18:29:33 +02:00
|
|
|
import 'package:nc_photos/use_case/download_preview.dart';
|
2022-10-15 16:29:18 +02:00
|
|
|
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
2022-05-07 12:27:26 +02:00
|
|
|
import 'package:nc_photos/use_case/share_local.dart';
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:nc_photos/widget/processing_dialog.dart';
|
2021-10-08 20:39:53 +02:00
|
|
|
import 'package:nc_photos/widget/share_link_multiple_files_dialog.dart';
|
2021-09-28 08:36:00 +02:00
|
|
|
import 'package:nc_photos/widget/share_method_dialog.dart';
|
|
|
|
import 'package:nc_photos/widget/simple_input_dialog.dart';
|
2022-05-03 12:44:48 +02:00
|
|
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
2022-12-16 16:01:04 +01:00
|
|
|
import 'package:np_codegen/np_codegen.dart';
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
|
2022-12-16 16:01:04 +01:00
|
|
|
part 'share_handler.g.dart';
|
|
|
|
|
2021-07-10 17:06:04 +02:00
|
|
|
/// Handle sharing to other apps
|
2022-12-16 16:01:04 +01:00
|
|
|
@npLog
|
2021-07-10 17:06:04 +02:00
|
|
|
class ShareHandler {
|
2022-10-15 16:29:18 +02:00
|
|
|
ShareHandler(
|
|
|
|
this._c, {
|
2021-10-08 20:39:53 +02:00
|
|
|
required this.context,
|
|
|
|
this.clearSelection,
|
2022-10-15 16:29:18 +02:00
|
|
|
}) : assert(require(_c)),
|
|
|
|
assert(InflateFileDescriptor.require(_c));
|
|
|
|
|
|
|
|
static bool require(DiContainer c) => true;
|
2021-10-08 20:39:53 +02:00
|
|
|
|
2022-05-07 12:27:26 +02:00
|
|
|
Future<void> shareLocalFiles(List<LocalFile> files) async {
|
|
|
|
if (!isSelectionCleared) {
|
|
|
|
clearSelection?.call();
|
|
|
|
}
|
|
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
|
|
var hasShownError = false;
|
|
|
|
await ShareLocal(c)(
|
|
|
|
files,
|
|
|
|
onFailure: (f, e, stackTrace) {
|
|
|
|
if (e != null) {
|
|
|
|
_log.shout(
|
|
|
|
"[shareLocalFiles] Failed while sharing file: ${logFilename(f.logTag)}",
|
|
|
|
e,
|
|
|
|
stackTrace);
|
|
|
|
if (!hasShownError) {
|
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(exception_util.toUserString(e)),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
hasShownError = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-10-15 16:29:18 +02:00
|
|
|
Future<void> shareFiles(Account account, List<FileDescriptor> fds) async {
|
|
|
|
final files = await InflateFileDescriptor(_c)(account, fds);
|
2021-10-08 20:39:53 +02:00
|
|
|
try {
|
2022-09-09 18:29:33 +02:00
|
|
|
final method = await _askShareMethod(files);
|
2021-10-08 20:39:53 +02:00
|
|
|
if (method == null) {
|
|
|
|
// user canceled
|
|
|
|
return;
|
|
|
|
} else if (method == ShareMethod.publicLink) {
|
|
|
|
return await _shareAsLink(account, files, false);
|
|
|
|
} else if (method == ShareMethod.passwordLink) {
|
|
|
|
return await _shareAsLink(account, files, true);
|
2022-09-09 18:29:33 +02:00
|
|
|
} else if (method == ShareMethod.preview) {
|
|
|
|
return await _shareAsPreview(account, files);
|
2021-10-08 20:39:53 +02:00
|
|
|
} else {
|
|
|
|
return await _shareAsFile(account, files);
|
|
|
|
}
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout("[shareFiles] Failed while sharing files", e, stackTrace);
|
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(exception_util.toUserString(e)),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
} finally {
|
|
|
|
if (!isSelectionCleared) {
|
|
|
|
clearSelection?.call();
|
|
|
|
}
|
2021-09-28 08:36:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-09 18:29:33 +02:00
|
|
|
Future<ShareMethod?> _askShareMethod(List<File> files) {
|
2021-09-28 08:36:00 +02:00
|
|
|
return showDialog<ShareMethod>(
|
2022-09-09 18:29:33 +02:00
|
|
|
context: context,
|
|
|
|
builder: (context) => ShareMethodDialog(
|
|
|
|
isSupportPerview: files.any((f) => file_util.isSupportedImageFormat(f)),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _shareAsPreview(Account account, List<File> files) async {
|
|
|
|
assert(platform_k.isAndroid);
|
|
|
|
final controller = StreamController<String>();
|
|
|
|
unawaited(
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (context) => StreamBuilder(
|
|
|
|
stream: controller.stream,
|
|
|
|
builder: (context, snapshot) => ProcessingDialog(
|
|
|
|
text: L10n.global().shareDownloadingDialogContent +
|
|
|
|
(snapshot.hasData ? " ${snapshot.data}" : ""),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
final results = <Tuple2<File, dynamic>>[];
|
|
|
|
for (final pair in files.withIndex()) {
|
|
|
|
final i = pair.item1, f = pair.item2;
|
|
|
|
controller.add("($i/${files.length})");
|
|
|
|
try {
|
|
|
|
final dynamic uri;
|
|
|
|
if (file_util.isSupportedImageFormat(f) &&
|
|
|
|
f.contentType != "image/gif") {
|
|
|
|
uri = await DownloadPreview()(account, f);
|
|
|
|
} else {
|
|
|
|
uri = await DownloadFile()(account, f);
|
|
|
|
}
|
|
|
|
results.add(Tuple2(f, uri));
|
|
|
|
} catch (e, stacktrace) {
|
|
|
|
_log.shout(
|
|
|
|
"[_shareAsPreview] Failed while DownloadPreview", e, stacktrace);
|
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(exception_util.toUserString(e)),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// dismiss the dialog
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
|
|
|
final share = AndroidFileShare(
|
|
|
|
results.map((e) => e.item2 as String).toList(),
|
|
|
|
results.map((e) => e.item1.contentType).toList());
|
|
|
|
return share.share();
|
2021-09-28 08:36:00 +02:00
|
|
|
}
|
|
|
|
|
2021-10-08 20:39:53 +02:00
|
|
|
Future<void> _shareAsFile(Account account, List<File> files) async {
|
2021-10-09 10:42:18 +02:00
|
|
|
assert(platform_k.isAndroid);
|
2021-09-19 23:18:20 +02:00
|
|
|
final controller = StreamController<String>();
|
2022-07-28 18:59:26 +02:00
|
|
|
unawaited(
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (context) => StreamBuilder(
|
|
|
|
stream: controller.stream,
|
|
|
|
builder: (context, snapshot) => ProcessingDialog(
|
|
|
|
text: L10n.global().shareDownloadingDialogContent +
|
|
|
|
(snapshot.hasData ? " ${snapshot.data}" : ""),
|
|
|
|
),
|
2021-09-19 23:18:20 +02:00
|
|
|
),
|
|
|
|
),
|
2021-07-10 17:06:04 +02:00
|
|
|
);
|
|
|
|
final results = <Tuple2<File, dynamic>>[];
|
2021-09-19 23:18:20 +02:00
|
|
|
for (final pair in files.withIndex()) {
|
|
|
|
final i = pair.item1, f = pair.item2;
|
2021-10-31 14:08:07 +01:00
|
|
|
controller.add("($i/${files.length})");
|
2021-07-10 17:06:04 +02:00
|
|
|
try {
|
2021-09-19 12:59:45 +02:00
|
|
|
results.add(Tuple2(
|
|
|
|
f,
|
|
|
|
await DownloadFile()(
|
|
|
|
account,
|
|
|
|
f,
|
|
|
|
shouldNotify: false,
|
|
|
|
)));
|
2021-07-10 17:06:04 +02:00
|
|
|
} on PermissionException catch (_) {
|
2021-10-08 20:39:53 +02:00
|
|
|
_log.warning("[_shareAsFile] Permission not granted");
|
2021-07-10 17:06:04 +02:00
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
2022-05-03 07:27:20 +02:00
|
|
|
content: Text(L10n.global().errorNoStoragePermission),
|
2021-07-10 17:06:04 +02:00
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
// dismiss the dialog
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
rethrow;
|
|
|
|
} catch (e, stacktrace) {
|
2021-10-08 20:39:53 +02:00
|
|
|
_log.shout("[_shareAsFile] Failed while downloadFile", e, stacktrace);
|
2021-07-10 17:06:04 +02:00
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
2021-08-29 13:51:43 +02:00
|
|
|
content: Text(exception_util.toUserString(e)),
|
2021-07-10 17:06:04 +02:00
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// dismiss the dialog
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
2021-09-28 08:36:00 +02:00
|
|
|
final share = AndroidFileShare(
|
|
|
|
results.map((e) => e.item2 as String).toList(),
|
2021-07-10 17:06:04 +02:00
|
|
|
results.map((e) => e.item1.contentType).toList());
|
2022-07-28 18:59:26 +02:00
|
|
|
return share.share();
|
2021-07-10 17:06:04 +02:00
|
|
|
}
|
|
|
|
|
2021-10-08 20:39:53 +02:00
|
|
|
Future<void> _shareAsLink(
|
|
|
|
Account account, List<File> files, bool isPasswordProtected) async {
|
|
|
|
if (files.length == 1) {
|
|
|
|
String? password;
|
|
|
|
if (isPasswordProtected) {
|
|
|
|
password = await _askPassword();
|
|
|
|
if (password == null) {
|
|
|
|
// user canceled
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return _shareFileAsLink(account, files.first, password);
|
|
|
|
} else {
|
|
|
|
final result = await _askDirDetail(context, isPasswordProtected);
|
|
|
|
if (result == null) {
|
|
|
|
// user canceled
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_log.info("[_shareAsLink] Share as folder: ${result.albumName}");
|
2022-07-25 07:04:22 +02:00
|
|
|
SnackBarManager().showSnackBar(
|
|
|
|
SnackBar(
|
|
|
|
content: Text(L10n.global().createShareProgressText),
|
|
|
|
duration: k.snackBarDurationShort,
|
|
|
|
),
|
|
|
|
canBeReplaced: true,
|
|
|
|
);
|
2021-10-08 20:39:53 +02:00
|
|
|
clearSelection?.call();
|
|
|
|
isSelectionCleared = true;
|
2021-09-28 08:36:00 +02:00
|
|
|
|
2022-07-05 22:20:24 +02:00
|
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
|
|
final path = await _createDir(c.fileRepo, account, result.albumName);
|
|
|
|
await _copyFilesToDir(c.fileRepo, account, files, path);
|
2021-10-08 20:39:53 +02:00
|
|
|
return _shareFileAsLink(
|
|
|
|
account,
|
|
|
|
File(
|
|
|
|
path: path,
|
|
|
|
isCollection: true,
|
|
|
|
),
|
|
|
|
result.password,
|
|
|
|
);
|
2021-09-28 08:36:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-08 20:39:53 +02:00
|
|
|
Future<void> _shareFileAsLink(
|
|
|
|
Account account, File file, String? password) async {
|
2021-09-28 08:36:00 +02:00
|
|
|
final shareRepo = ShareRepo(ShareRemoteDataSource());
|
|
|
|
try {
|
|
|
|
final share = await CreateLinkShare(shareRepo)(
|
|
|
|
account,
|
|
|
|
file,
|
|
|
|
password: password,
|
|
|
|
);
|
|
|
|
await Clipboard.setData(ClipboardData(text: share.url));
|
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(L10n.global().linkCopiedNotification),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
|
2021-11-25 12:19:08 +01:00
|
|
|
if (platform_k.isAndroid) {
|
|
|
|
final textShare = AndroidTextShare(share.url!);
|
2022-07-28 18:59:26 +02:00
|
|
|
await textShare.share();
|
2021-11-25 12:19:08 +01:00
|
|
|
}
|
2021-09-28 08:36:00 +02:00
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout(
|
2021-10-08 20:39:53 +02:00
|
|
|
"[_shareFileAsLink] Failed while CreateLinkShare", e, stackTrace);
|
2021-09-28 08:36:00 +02:00
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(exception_util.toUserString(e)),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-08 20:39:53 +02:00
|
|
|
Future<String?> _askPassword() {
|
2021-09-28 08:36:00 +02:00
|
|
|
return showDialog<String>(
|
|
|
|
context: context,
|
|
|
|
builder: (context) => SimpleInputDialog(
|
|
|
|
hintText: L10n.global().passwordInputHint,
|
|
|
|
buttonText: MaterialLocalizations.of(context).okButtonLabel,
|
|
|
|
validator: (value) {
|
|
|
|
if (value?.isNotEmpty != true) {
|
|
|
|
return L10n.global().passwordInputInvalidEmpty;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
obscureText: true,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-08 20:39:53 +02:00
|
|
|
Future<ShareLinkMultipleFilesDialogResult?> _askDirDetail(
|
|
|
|
BuildContext context, bool isPasswordProtected) {
|
|
|
|
return showDialog<ShareLinkMultipleFilesDialogResult>(
|
|
|
|
context: context,
|
|
|
|
builder: (_) => ShareLinkMultipleFilesDialog(
|
|
|
|
shouldAskPassword: isPasswordProtected,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> _createDir(
|
|
|
|
FileRepo fileRepo, Account account, String name) async {
|
|
|
|
// add a intermediate dir to allow shared dirs having the same name. Since
|
|
|
|
// the dir names are public, we can't add random pre/suffix
|
|
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
final random = Random().nextInt(0xFFFFFF);
|
|
|
|
final dirName =
|
|
|
|
"${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, "0")}";
|
|
|
|
final path =
|
|
|
|
"${remote_storage_util.getRemoteLinkSharesDir(account)}/$dirName/$name";
|
|
|
|
await CreateDir(fileRepo)(account, path);
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _copyFilesToDir(FileRepo fileRepo, Account account,
|
|
|
|
List<File> files, String dirPath) async {
|
|
|
|
var failureCount = 0;
|
|
|
|
for (final f in files) {
|
|
|
|
try {
|
2021-10-17 11:48:00 +02:00
|
|
|
await Copy(fileRepo)(account, f, "$dirPath/${f.filename}");
|
2021-10-08 20:39:53 +02:00
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.severe(
|
|
|
|
"[_copyFilesToDir] Failed while copying file: $f", e, stackTrace);
|
|
|
|
++failureCount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (failureCount != 0) {
|
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(L10n.global().copyItemsFailureNotification(failureCount)),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-15 16:29:18 +02:00
|
|
|
final DiContainer _c;
|
2021-10-08 20:39:53 +02:00
|
|
|
final BuildContext context;
|
|
|
|
final VoidCallback? clearSelection;
|
|
|
|
var isSelectionCleared = false;
|
2021-07-10 17:06:04 +02:00
|
|
|
}
|