import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/np_api_util.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/remove_share.dart'; import 'package:nc_photos/widget/list_tile_center_leading.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:path/path.dart' as path_lib; part 'shared_file_viewer.g.dart'; class SharedFileViewerArguments { SharedFileViewerArguments(this.account, this.file, this.shares); final Account account; final File file; final List shares; } /// Handle shares associated with a [File] class SharedFileViewer extends StatefulWidget { static const routeName = "/shared-file-viewer"; static Route buildRoute(SharedFileViewerArguments args) => MaterialPageRoute( builder: (context) => SharedFileViewer.fromArgs(args), ); const SharedFileViewer({ Key? key, required this.account, required this.file, required this.shares, }) : super(key: key); SharedFileViewer.fromArgs(SharedFileViewerArguments args, {Key? key}) : this( key: key, account: args.account, file: args.file, shares: args.shares, ); @override createState() => _SharedFileViewerState(); final Account account; final File file; final List shares; } @npLog class _SharedFileViewerState extends State { @override build(BuildContext context) { return Scaffold( body: _buildContent(context), ); } Widget _buildContent(BuildContext context) { final previewUrl = api_util.getFilePreviewUrl( widget.account, widget.file, width: k.photoLargeSize, height: k.photoLargeSize, isKeepAspectRatio: true, ); return CustomScrollView( slivers: [ SliverAppBar( title: Text(path_lib.withoutExtension(widget.file.filename)), pinned: true, ), if (widget.file.isCollection != true) SliverToBoxAdapter( child: SizedBox( height: 256, child: FittedBox( alignment: Alignment.center, fit: BoxFit.cover, clipBehavior: Clip.hardEdge, child: CachedNetworkImage( cacheManager: LargeImageCacheManager.inst, imageUrl: previewUrl, httpHeaders: { "Authorization": AuthUtil.fromAccount(widget.account).toHeaderValue(), }, fadeInDuration: const Duration(), filterQuality: FilterQuality.high, errorWidget: (context, url, error) { // just leave it empty return Container(); }, imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, ), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: Text( L10n.global().locationLabel, style: TextStyle( color: Theme.of(context).colorScheme.primary, ), ), ), ), SliverToBoxAdapter( child: ListTile( title: Text(widget.file.strippedPath), ), ), if (widget.shares.first.uidOwner == widget.account.userId) ...[ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: Text( L10n.global().sharedWithLabel, style: TextStyle( color: Theme.of(context).colorScheme.primary, ), ), ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => _buildShareItem(context, _shares[index]), childCount: _shares.length, ), ), ], ], ); } Widget _buildShareItem(BuildContext context, Share share) { final dateStr = DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY, Localizations.localeOf(context).languageCode) .format(share.stime.toLocal()); return ListTile( title: Text(_getShareTitle(share)), subtitle: Text(dateStr), leading: ListTileCenterLeading( child: Icon(_getShareIcon(share)), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (share.shareType == ShareType.publicLink) IconButton( onPressed: () => _onItemCopyPressed(share), icon: Tooltip( message: MaterialLocalizations.of(context).copyButtonLabel, child: const Icon(Icons.copy_outlined), ), ), PopupMenuButton<_ItemMenuOption>( tooltip: MaterialLocalizations.of(context).moreButtonTooltip, itemBuilder: (_) => [ PopupMenuItem( value: _ItemMenuOption.unshare, child: Text(L10n.global().unshareTooltip), ), ], onSelected: (option) => _onItemMenuOptionSelected(context, share, option), ), ], ), ); } Future _onItemCopyPressed(Share share) async { await Clipboard.setData(ClipboardData(text: share.url!)); SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global().linkCopiedNotification), duration: k.snackBarDurationNormal, )); } void _onItemMenuOptionSelected( BuildContext context, Share share, _ItemMenuOption option) { switch (option) { case _ItemMenuOption.unshare: _onItemUnsharePressed(context, share); break; } } Future _onItemUnsharePressed(BuildContext context, Share share) async { final shareRepo = ShareRepo(ShareRemoteDataSource()); try { await RemoveShare(shareRepo)(widget.account, share); SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global().unshareSuccessNotification), duration: k.snackBarDurationNormal, )); if (_shares.length == 1) { // removing last share try { if (widget.file.path.startsWith( remote_storage_util.getRemoteLinkSharesDir(widget.account) + "/")) { // file is a link share dir created by us if (await _askDeleteLinkShareDir()) { await _deleteLinkShareDir(); } } } finally { if (mounted) { Navigator.of(context).pop(); } } } else { setState(() { _shares.remove(share); }); } } catch (e, stackTrace) { _log.shout( "[_onItemUnsharePressed] Failed while RemoveShare", e, stackTrace); SnackBarManager().showSnackBar(SnackBar( content: Text(exception_util.toUserString(e)), duration: k.snackBarDurationNormal, )); } } Future _askDeleteLinkShareDir() async { final result = await showDialog( context: context, builder: (_) => AlertDialog( title: Text(L10n.global().unshareLinkShareDirDialogTitle), content: Text(L10n.global().unshareLinkShareDirDialogContent), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(MaterialLocalizations.of(context).cancelButtonLabel)), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text(MaterialLocalizations.of(context).okButtonLabel)), ], ), ); return result == true; } Future _deleteLinkShareDir() { // the upper dir is also part of this link share dir var dirPath = path_lib.dirname(widget.file.path); // make sure we are not accidentally deleting other dirs if (!dirPath.startsWith( remote_storage_util.getRemoteLinkSharesDir(widget.account) + "/")) { _log.shout( "[_deleteLinkShareDir] Invalid upper dir to be deleted: $dirPath"); dirPath = widget.file.path; } return Remove(KiwiContainer().resolve())( widget.account, [widget.file.copyWith(path: dirPath)], shouldCleanUp: false, ); } IconData? _getShareIcon(Share share) { switch (share.shareType) { case ShareType.user: case ShareType.email: case ShareType.federatedCloudShare: return Icons.person_outlined; case ShareType.group: case ShareType.circle: return Icons.group_outlined; case ShareType.publicLink: return Icons.link_outlined; case ShareType.talk: return Icons.sms_outlined; default: return null; } } String _getShareTitle(Share share) { if (share.shareType == ShareType.publicLink) { if (share.url!.startsWith(widget.account.url)) { return share.url!.substring(widget.account.url.length); } else { return share.url!; } } else { return share.shareWithDisplayName; } } late final List _shares = List.of(widget.shares); } enum _ItemMenuOption { unshare, }