2021-10-06 22:32:36 +02:00
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:nc_photos/account.dart';
|
|
|
|
import 'package:nc_photos/api/api.dart';
|
|
|
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
|
|
|
import 'package:nc_photos/app_localizations.dart';
|
|
|
|
import 'package:nc_photos/bloc/list_sharing.dart';
|
|
|
|
import 'package:nc_photos/cache_manager_util.dart';
|
2021-10-12 12:15:58 +02:00
|
|
|
import 'package:nc_photos/entity/album.dart';
|
2021-10-12 18:54:21 +02:00
|
|
|
import 'package:nc_photos/entity/file.dart';
|
2021-10-12 12:15:58 +02:00
|
|
|
import 'package:nc_photos/entity/file/data_source.dart';
|
2021-10-06 22:32:36 +02:00
|
|
|
import 'package:nc_photos/entity/share.dart';
|
|
|
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
|
|
|
import 'package:nc_photos/iterable_extension.dart';
|
|
|
|
import 'package:nc_photos/k.dart' as k;
|
2021-10-12 18:54:21 +02:00
|
|
|
import 'package:nc_photos/pref.dart';
|
2021-10-06 22:32:36 +02:00
|
|
|
import 'package:nc_photos/snack_bar_manager.dart';
|
|
|
|
import 'package:nc_photos/theme.dart';
|
2021-10-12 18:54:21 +02:00
|
|
|
import 'package:nc_photos/use_case/import_potential_shared_album.dart';
|
2021-10-12 12:15:58 +02:00
|
|
|
import 'package:nc_photos/widget/album_browser_util.dart' as album_browser_util;
|
2021-10-06 22:32:36 +02:00
|
|
|
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
|
|
|
import 'package:nc_photos/widget/shared_file_viewer.dart';
|
2021-10-17 12:21:50 +02:00
|
|
|
import 'package:nc_photos/widget/unbounded_list_tile.dart';
|
2021-10-06 22:32:36 +02:00
|
|
|
|
|
|
|
class SharingBrowserArguments {
|
|
|
|
SharingBrowserArguments(this.account);
|
|
|
|
|
|
|
|
final Account account;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Show a list of all shares associated with this account
|
|
|
|
class SharingBrowser extends StatefulWidget {
|
|
|
|
static const routeName = "/sharing-browser";
|
|
|
|
|
|
|
|
static Route buildRoute(SharingBrowserArguments args) => MaterialPageRoute(
|
|
|
|
builder: (context) => SharingBrowser.fromArgs(args),
|
|
|
|
);
|
|
|
|
|
|
|
|
const SharingBrowser({
|
|
|
|
Key? key,
|
|
|
|
required this.account,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
SharingBrowser.fromArgs(SharingBrowserArguments args, {Key? key})
|
|
|
|
: this(
|
|
|
|
key: key,
|
|
|
|
account: args.account,
|
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
createState() => _SharingBrowserState();
|
|
|
|
|
|
|
|
final Account account;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SharingBrowserState extends State<SharingBrowser> {
|
|
|
|
@override
|
|
|
|
initState() {
|
|
|
|
super.initState();
|
2021-10-27 22:40:54 +02:00
|
|
|
if (Pref().isLabEnableSharedAlbumOr(false)) {
|
2021-10-12 18:54:21 +02:00
|
|
|
_importPotentialSharedAlbum().whenComplete(() {
|
|
|
|
_initBloc();
|
|
|
|
});
|
2021-10-27 22:40:54 +02:00
|
|
|
Pref().setNewSharedAlbum(false);
|
2021-10-12 18:54:21 +02:00
|
|
|
} else {
|
|
|
|
_initBloc();
|
|
|
|
}
|
2021-10-06 22:32:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
|
|
|
return AppTheme(
|
|
|
|
child: Scaffold(
|
|
|
|
body: BlocListener<ListSharingBloc, ListSharingBlocState>(
|
|
|
|
bloc: _bloc,
|
|
|
|
listener: (context, state) => _onStateChange(context, state),
|
|
|
|
child: BlocBuilder<ListSharingBloc, ListSharingBlocState>(
|
|
|
|
bloc: _bloc,
|
|
|
|
builder: (context, state) => _buildContent(context, state),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _initBloc() {
|
|
|
|
if (_bloc.state is ListSharingBlocInit) {
|
|
|
|
_log.info("[_initBloc] Initialize bloc");
|
|
|
|
} else {
|
|
|
|
// process the current state
|
|
|
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
|
|
|
setState(() {
|
|
|
|
_onStateChange(context, _bloc.state);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
_reqQuery();
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildContent(BuildContext context, ListSharingBlocState state) {
|
|
|
|
if ((state is ListSharingBlocSuccess || state is ListSharingBlocFailure) &&
|
|
|
|
state.items.isEmpty) {
|
|
|
|
return _buildEmptyContent(context);
|
|
|
|
} else {
|
2021-10-09 21:35:04 +02:00
|
|
|
return Stack(
|
|
|
|
children: [
|
|
|
|
Theme(
|
|
|
|
data: Theme.of(context).copyWith(
|
|
|
|
colorScheme: Theme.of(context).colorScheme.copyWith(
|
|
|
|
secondary: AppTheme.getOverscrollIndicatorColor(context),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
child: CustomScrollView(
|
|
|
|
slivers: [
|
|
|
|
SliverAppBar(
|
|
|
|
title: Text(L10n.global().collectionSharingLabel),
|
|
|
|
floating: true,
|
|
|
|
),
|
|
|
|
SliverList(
|
|
|
|
delegate: SliverChildBuilderDelegate(
|
|
|
|
(context, index) => _buildItem(context, _items[index]),
|
|
|
|
childCount: _items.length,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2021-10-06 22:32:36 +02:00
|
|
|
),
|
|
|
|
),
|
2021-10-09 21:35:04 +02:00
|
|
|
if (state is ListSharingBlocLoading)
|
|
|
|
const Align(
|
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
|
child: LinearProgressIndicator(),
|
|
|
|
),
|
2021-10-06 22:32:36 +02:00
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildEmptyContent(BuildContext context) {
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
AppBar(
|
|
|
|
title: Text(L10n.global().collectionSharingLabel),
|
|
|
|
elevation: 0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: EmptyListIndicator(
|
|
|
|
icon: Icons.share_outlined,
|
|
|
|
text: L10n.global().listEmptyText,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-12 12:15:58 +02:00
|
|
|
Widget _buildFileItem(BuildContext context, List<ListSharingItem> shares) {
|
|
|
|
assert(shares.first is ListSharingFile);
|
|
|
|
final dateStr =
|
|
|
|
_getDateFormat(context).format(shares.first.share.stime.toLocal());
|
|
|
|
final firstItem = shares.first as ListSharingFile;
|
2021-10-11 19:06:49 +02:00
|
|
|
return _ListTile(
|
|
|
|
leading: shares.first.share.itemType == ShareItemType.folder
|
2021-10-17 12:16:23 +02:00
|
|
|
? SizedBox(
|
2021-10-12 12:15:58 +02:00
|
|
|
height: _leadingSize,
|
|
|
|
width: _leadingSize,
|
2021-10-11 19:07:34 +02:00
|
|
|
child: Icon(
|
|
|
|
Icons.folder,
|
|
|
|
size: 32,
|
2021-10-17 12:16:23 +02:00
|
|
|
color: AppTheme.getUnfocusedIconColor(context),
|
2021-10-11 19:07:34 +02:00
|
|
|
),
|
2021-10-11 19:06:49 +02:00
|
|
|
)
|
|
|
|
: CachedNetworkImage(
|
2021-10-12 12:15:58 +02:00
|
|
|
width: _leadingSize,
|
|
|
|
height: _leadingSize,
|
2021-10-11 19:06:49 +02:00
|
|
|
cacheManager: ThumbnailCacheManager.inst,
|
|
|
|
imageUrl: api_util.getFilePreviewUrl(
|
2021-10-12 12:15:58 +02:00
|
|
|
widget.account, firstItem.file,
|
2021-10-11 19:06:49 +02:00
|
|
|
width: k.photoThumbSize, height: k.photoThumbSize),
|
|
|
|
httpHeaders: {
|
|
|
|
"Authorization":
|
|
|
|
Api.getAuthorizationHeaderValue(widget.account),
|
|
|
|
},
|
|
|
|
fadeInDuration: const Duration(),
|
|
|
|
filterQuality: FilterQuality.high,
|
|
|
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
2021-10-17 12:17:47 +02:00
|
|
|
errorWidget: (context, url, error) => Icon(
|
|
|
|
Icons.image_not_supported,
|
|
|
|
size: 32,
|
|
|
|
color: AppTheme.getUnfocusedIconColor(context),
|
|
|
|
),
|
2021-10-11 19:06:49 +02:00
|
|
|
),
|
|
|
|
label: shares.first.share.filename,
|
2021-11-12 22:13:02 +01:00
|
|
|
description: shares.first.share.uidOwner == widget.account.username
|
|
|
|
? L10n.global().fileLastSharedDescription(dateStr)
|
|
|
|
: L10n.global().fileLastSharedByOthersDescription(
|
|
|
|
shares.first.share.displaynameOwner, dateStr),
|
2021-10-11 19:06:49 +02:00
|
|
|
trailing: (shares.any((element) => element.share.url?.isNotEmpty == true))
|
2021-10-17 12:16:23 +02:00
|
|
|
? Icon(
|
|
|
|
Icons.link,
|
|
|
|
color: AppTheme.getUnfocusedIconColor(context),
|
|
|
|
)
|
2021-10-11 19:06:49 +02:00
|
|
|
: null,
|
2021-10-06 22:32:36 +02:00
|
|
|
onTap: () {
|
|
|
|
Navigator.of(context).pushNamed(SharedFileViewer.routeName,
|
|
|
|
arguments: SharedFileViewerArguments(
|
|
|
|
widget.account,
|
2021-10-12 12:15:58 +02:00
|
|
|
firstItem.file,
|
2021-10-06 22:32:36 +02:00
|
|
|
shares.map((e) => e.share).toList(),
|
|
|
|
));
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-12 12:15:58 +02:00
|
|
|
Widget _buildAlbumItem(BuildContext context, List<ListSharingItem> shares) {
|
|
|
|
assert(shares.first is ListSharingAlbum);
|
|
|
|
final dateStr =
|
|
|
|
_getDateFormat(context).format(shares.first.share.stime.toLocal());
|
|
|
|
final firstItem = shares.first as ListSharingAlbum;
|
|
|
|
final cover = firstItem.album.coverProvider.getCover(firstItem.album);
|
|
|
|
return _ListTile(
|
|
|
|
leading: cover == null
|
2021-11-18 16:41:16 +01:00
|
|
|
? SizedBox(
|
|
|
|
height: _leadingSize,
|
|
|
|
width: _leadingSize,
|
|
|
|
child: Icon(
|
|
|
|
Icons.photo_album_outlined,
|
|
|
|
size: 32,
|
|
|
|
color: AppTheme.getUnfocusedIconColor(context),
|
|
|
|
),
|
2021-10-12 12:15:58 +02:00
|
|
|
)
|
|
|
|
: CachedNetworkImage(
|
|
|
|
width: _leadingSize,
|
|
|
|
height: _leadingSize,
|
|
|
|
cacheManager: ThumbnailCacheManager.inst,
|
|
|
|
imageUrl: api_util.getFilePreviewUrl(widget.account, cover,
|
|
|
|
width: k.photoThumbSize, height: k.photoThumbSize),
|
|
|
|
httpHeaders: {
|
|
|
|
"Authorization":
|
|
|
|
Api.getAuthorizationHeaderValue(widget.account),
|
|
|
|
},
|
|
|
|
fadeInDuration: const Duration(),
|
|
|
|
filterQuality: FilterQuality.high,
|
|
|
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
2021-10-17 12:16:23 +02:00
|
|
|
errorWidget: (context, url, error) => Icon(
|
2021-10-12 12:15:58 +02:00
|
|
|
Icons.panorama,
|
|
|
|
size: 32,
|
2021-10-17 12:16:23 +02:00
|
|
|
color: AppTheme.getUnfocusedIconColor(context),
|
2021-10-12 12:15:58 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
label: firstItem.album.name,
|
2021-11-12 22:13:02 +01:00
|
|
|
description: shares.first.share.uidOwner == widget.account.username
|
|
|
|
? L10n.global().fileLastSharedDescription(dateStr)
|
|
|
|
: L10n.global().albumLastSharedByOthersDescription(
|
|
|
|
shares.first.share.displaynameOwner, dateStr),
|
2021-10-17 12:16:23 +02:00
|
|
|
trailing: Icon(
|
|
|
|
Icons.photo_album_outlined,
|
|
|
|
color: AppTheme.getUnfocusedIconColor(context),
|
|
|
|
),
|
2021-10-12 12:15:58 +02:00
|
|
|
onTap: () =>
|
|
|
|
_onAlbumShareItemTap(context, shares.first as ListSharingAlbum),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildItem(BuildContext context, List<ListSharingItem> shares) {
|
|
|
|
if (shares.first is ListSharingFile) {
|
|
|
|
return _buildFileItem(context, shares);
|
|
|
|
} else if (shares.first is ListSharingAlbum) {
|
|
|
|
return _buildAlbumItem(context, shares);
|
|
|
|
} else {
|
2021-10-17 12:16:53 +02:00
|
|
|
throw StateError("Unknown item type: ${shares.first.runtimeType}");
|
2021-10-12 12:15:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-06 22:32:36 +02:00
|
|
|
void _onStateChange(BuildContext context, ListSharingBlocState state) {
|
|
|
|
if (state is ListSharingBlocInit) {
|
|
|
|
_items = [];
|
|
|
|
} else if (state is ListSharingBlocSuccess ||
|
|
|
|
state is ListSharingBlocLoading) {
|
|
|
|
_transformItems(state.items);
|
|
|
|
} else if (state is ListSharingBlocFailure) {
|
|
|
|
_transformItems(state.items);
|
|
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
|
|
content: Text(exception_util.toUserString(state.exception)),
|
|
|
|
duration: k.snackBarDurationNormal,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-12 12:15:58 +02:00
|
|
|
Future<void> _onAlbumShareItemTap(
|
2021-10-18 22:26:35 +02:00
|
|
|
BuildContext context, ListSharingAlbum share) {
|
2021-11-21 14:36:13 +01:00
|
|
|
return album_browser_util.push(context, widget.account, share.album);
|
2021-10-12 12:15:58 +02:00
|
|
|
}
|
|
|
|
|
2021-10-06 22:32:36 +02:00
|
|
|
void _transformItems(List<ListSharingItem> items) {
|
|
|
|
// group shares of the same file
|
|
|
|
final map = <String, List<ListSharingItem>>{};
|
|
|
|
for (final i in items) {
|
2021-11-12 22:13:02 +01:00
|
|
|
final isSharedByMe = (i.share.uidOwner == widget.account.username);
|
2021-10-11 19:03:37 +02:00
|
|
|
final groupKey = "${i.share.path}?$isSharedByMe";
|
|
|
|
map[groupKey] ??= <ListSharingItem>[];
|
|
|
|
map[groupKey]!.add(i);
|
2021-10-06 22:32:36 +02:00
|
|
|
}
|
|
|
|
// sort the sub-lists
|
|
|
|
for (final list in map.values) {
|
|
|
|
list.sort((a, b) => b.share.stime.compareTo(a.share.stime));
|
|
|
|
}
|
|
|
|
// then sort the map and convert it to list
|
|
|
|
_items = map.entries
|
|
|
|
.sorted((a, b) =>
|
|
|
|
b.value.first.share.stime.compareTo(a.value.first.share.stime))
|
|
|
|
.map((e) => e.value)
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _reqQuery() {
|
|
|
|
_bloc.add(ListSharingBlocQuery(widget.account));
|
|
|
|
}
|
|
|
|
|
2021-10-12 18:54:21 +02:00
|
|
|
Future<List<Album>> _importPotentialSharedAlbum() async {
|
|
|
|
const fileRepo = FileRepo(FileWebdavDataSource());
|
|
|
|
// don't want the potential albums to be cached at this moment
|
|
|
|
final albumRepo = AlbumRepo(AlbumRemoteDataSource());
|
|
|
|
try {
|
|
|
|
return await ImportPotentialSharedAlbum(fileRepo, albumRepo)(
|
2021-10-27 22:40:54 +02:00
|
|
|
widget.account, Pref().getAccountSettings(widget.account));
|
2021-10-12 18:54:21 +02:00
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout(
|
|
|
|
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
|
|
|
e,
|
|
|
|
stackTrace);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-06 22:32:36 +02:00
|
|
|
late final _bloc = ListSharingBloc.of(widget.account);
|
|
|
|
|
|
|
|
var _items = <List<ListSharingItem>>[];
|
|
|
|
|
|
|
|
static final _log = Logger("widget.sharing_browser._SharingBrowserState");
|
|
|
|
}
|
2021-10-11 19:06:49 +02:00
|
|
|
|
|
|
|
class _ListTile extends StatelessWidget {
|
|
|
|
const _ListTile({
|
|
|
|
required this.leading,
|
|
|
|
required this.label,
|
|
|
|
required this.description,
|
|
|
|
this.trailing,
|
|
|
|
required this.onTap,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
2021-10-17 12:21:50 +02:00
|
|
|
return UnboundedListTile(
|
|
|
|
leading: leading,
|
|
|
|
title: Text(
|
|
|
|
label,
|
|
|
|
maxLines: 1,
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2021-10-11 19:06:49 +02:00
|
|
|
),
|
2021-10-17 12:21:50 +02:00
|
|
|
subtitle: Text(description),
|
|
|
|
trailing: trailing,
|
|
|
|
onTap: onTap,
|
2021-10-11 19:06:49 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final Widget leading;
|
|
|
|
final String label;
|
|
|
|
final String description;
|
|
|
|
final Widget? trailing;
|
|
|
|
final VoidCallback onTap;
|
|
|
|
}
|
2021-10-12 12:15:58 +02:00
|
|
|
|
|
|
|
const _leadingSize = 56.0;
|
|
|
|
|
|
|
|
DateFormat _getDateFormat(BuildContext context) => DateFormat(
|
|
|
|
DateFormat.YEAR_ABBR_MONTH_DAY,
|
|
|
|
Localizations.localeOf(context).languageCode);
|