2021-05-06 13:29:20 +02:00
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
2021-12-10 05:53:22 +01:00
|
|
|
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
2021-05-06 13:29:20 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2022-06-06 19:37:46 +02:00
|
|
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
2021-10-04 15:53:03 +02:00
|
|
|
import 'package:intl/intl.dart';
|
2021-05-06 13:29:20 +02:00
|
|
|
import 'package:nc_photos/account.dart';
|
|
|
|
import 'package:nc_photos/api/api.dart';
|
2021-08-10 12:10:22 +02:00
|
|
|
import 'package:nc_photos/app_localizations.dart';
|
2021-09-16 12:10:50 +02:00
|
|
|
import 'package:nc_photos/cache_manager_util.dart';
|
2022-10-15 16:29:18 +02:00
|
|
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
2022-06-06 19:37:46 +02:00
|
|
|
import 'package:nc_photos/entity/local_file.dart';
|
|
|
|
import 'package:nc_photos/k.dart' as k;
|
|
|
|
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
|
2021-05-06 13:29:20 +02:00
|
|
|
import 'package:nc_photos/theme.dart';
|
2022-06-06 19:37:46 +02:00
|
|
|
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
|
|
|
|
|
|
|
abstract class PhotoListFileItem extends SelectableItem {
|
|
|
|
const PhotoListFileItem({
|
|
|
|
required this.fileIndex,
|
|
|
|
required this.file,
|
|
|
|
required this.shouldShowFavoriteBadge,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
get isTappable => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
get isSelectable => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
operator ==(Object other) =>
|
|
|
|
other is PhotoListFileItem && file.compareServerIdentity(other.file);
|
|
|
|
|
|
|
|
@override
|
2022-10-15 16:29:18 +02:00
|
|
|
get hashCode => file.fdPath.hashCode;
|
2022-06-06 19:37:46 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
toString() => "$runtimeType {"
|
2022-06-11 20:56:07 +02:00
|
|
|
"fileIndex: $fileIndex, "
|
2022-10-15 16:29:18 +02:00
|
|
|
"file: ${file.fdPath}, "
|
2022-06-11 20:56:07 +02:00
|
|
|
"shouldShowFavoriteBadge: $shouldShowFavoriteBadge, "
|
|
|
|
"}";
|
2022-06-06 19:37:46 +02:00
|
|
|
|
|
|
|
final int fileIndex;
|
2022-10-15 16:29:18 +02:00
|
|
|
final FileDescriptor file;
|
2022-06-06 19:37:46 +02:00
|
|
|
final bool shouldShowFavoriteBadge;
|
|
|
|
}
|
|
|
|
|
|
|
|
class PhotoListImageItem extends PhotoListFileItem {
|
|
|
|
const PhotoListImageItem({
|
|
|
|
required int fileIndex,
|
2022-10-15 16:29:18 +02:00
|
|
|
required FileDescriptor file,
|
2022-06-06 19:37:46 +02:00
|
|
|
required this.account,
|
|
|
|
required this.previewUrl,
|
|
|
|
required bool shouldShowFavoriteBadge,
|
|
|
|
}) : super(
|
|
|
|
fileIndex: fileIndex,
|
|
|
|
file: file,
|
|
|
|
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
buildWidget(BuildContext context) => PhotoListImage(
|
|
|
|
account: account,
|
|
|
|
previewUrl: previewUrl,
|
2022-10-15 16:29:18 +02:00
|
|
|
isGif: file.fdMime == "image/gif",
|
|
|
|
isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true,
|
2022-06-06 19:37:46 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
final Account account;
|
|
|
|
final String previewUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
class PhotoListVideoItem extends PhotoListFileItem {
|
|
|
|
const PhotoListVideoItem({
|
|
|
|
required int fileIndex,
|
2022-10-15 16:29:18 +02:00
|
|
|
required FileDescriptor file,
|
2022-06-06 19:37:46 +02:00
|
|
|
required this.account,
|
|
|
|
required this.previewUrl,
|
|
|
|
required bool shouldShowFavoriteBadge,
|
|
|
|
}) : super(
|
|
|
|
fileIndex: fileIndex,
|
|
|
|
file: file,
|
|
|
|
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
buildWidget(BuildContext context) => PhotoListVideo(
|
|
|
|
account: account,
|
|
|
|
previewUrl: previewUrl,
|
2022-10-15 16:29:18 +02:00
|
|
|
isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true,
|
2022-06-06 19:37:46 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
final Account account;
|
|
|
|
final String previewUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
class PhotoListDateItem extends SelectableItem {
|
|
|
|
const PhotoListDateItem({
|
|
|
|
required this.date,
|
|
|
|
this.isMonthOnly = false,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
get isTappable => false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
get isSelectable => false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
get staggeredTile => const StaggeredTile.extent(99, 32);
|
|
|
|
|
|
|
|
@override
|
|
|
|
buildWidget(BuildContext context) => PhotoListDate(
|
|
|
|
date: date,
|
|
|
|
isMonthOnly: isMonthOnly,
|
|
|
|
);
|
|
|
|
|
|
|
|
final DateTime date;
|
|
|
|
final bool isMonthOnly;
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class PhotoListLocalFileItem extends SelectableItem {
|
|
|
|
const PhotoListLocalFileItem({
|
|
|
|
required this.fileIndex,
|
|
|
|
required this.file,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
get isTappable => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
get isSelectable => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
operator ==(Object other) =>
|
|
|
|
other is PhotoListLocalFileItem && file.compareIdentity(other.file);
|
|
|
|
|
|
|
|
@override
|
|
|
|
get hashCode => file.identityHashCode;
|
|
|
|
|
|
|
|
final int fileIndex;
|
|
|
|
final LocalFile file;
|
|
|
|
}
|
|
|
|
|
|
|
|
class PhotoListLocalImageItem extends PhotoListLocalFileItem {
|
|
|
|
const PhotoListLocalImageItem({
|
|
|
|
required int fileIndex,
|
|
|
|
required LocalFile file,
|
|
|
|
}) : super(
|
|
|
|
fileIndex: fileIndex,
|
|
|
|
file: file,
|
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
buildWidget(BuildContext context) {
|
|
|
|
final ImageProvider provider;
|
|
|
|
if (file is LocalUriFile) {
|
|
|
|
provider = ContentUriImage((file as LocalUriFile).uri);
|
|
|
|
} else {
|
|
|
|
throw ArgumentError("Invalid file");
|
|
|
|
}
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(2),
|
|
|
|
child: FittedBox(
|
|
|
|
clipBehavior: Clip.hardEdge,
|
|
|
|
fit: BoxFit.cover,
|
2022-09-05 07:05:48 +02:00
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
color: AppTheme.getListItemBackgroundColor(context),
|
|
|
|
child: Image(
|
|
|
|
image: ResizeImage.resizeIfNeeded(
|
|
|
|
k.photoThumbSize, null, provider),
|
|
|
|
filterQuality: FilterQuality.high,
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
errorBuilder: (context, e, stackTrace) {
|
|
|
|
return Center(
|
|
|
|
child: Icon(
|
|
|
|
Icons.image_not_supported,
|
|
|
|
size: 64,
|
|
|
|
color: Colors.white.withOpacity(.8),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
alignment: AlignmentDirectional.bottomEnd,
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
child: const Icon(
|
|
|
|
Icons.cloud_off,
|
|
|
|
size: 20,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2022-06-06 19:37:46 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2021-05-06 13:29:20 +02:00
|
|
|
|
|
|
|
class PhotoListImage extends StatelessWidget {
|
|
|
|
const PhotoListImage({
|
2021-07-23 22:05:57 +02:00
|
|
|
Key? key,
|
|
|
|
required this.account,
|
|
|
|
required this.previewUrl,
|
2022-01-15 11:35:15 +01:00
|
|
|
this.padding = const EdgeInsets.all(2),
|
2021-06-22 07:24:37 +02:00
|
|
|
this.isGif = false,
|
2022-01-25 11:15:17 +01:00
|
|
|
this.isFavorite = false,
|
2022-09-07 11:37:50 +02:00
|
|
|
this.heroKey,
|
2021-05-06 13:29:20 +02:00
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
2022-09-07 11:37:50 +02:00
|
|
|
Widget buildPlaceholder() => Center(
|
|
|
|
child: Icon(
|
|
|
|
Icons.image_not_supported,
|
|
|
|
size: 64,
|
|
|
|
color: Colors.white.withOpacity(.8),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
Widget child;
|
|
|
|
if (previewUrl == null) {
|
|
|
|
child = buildPlaceholder();
|
|
|
|
} else {
|
|
|
|
child = CachedNetworkImage(
|
|
|
|
cacheManager: ThumbnailCacheManager.inst,
|
|
|
|
imageUrl: previewUrl!,
|
|
|
|
httpHeaders: {
|
|
|
|
"Authorization": Api.getAuthorizationHeaderValue(account),
|
|
|
|
},
|
|
|
|
fadeInDuration: const Duration(),
|
|
|
|
filterQuality: FilterQuality.high,
|
|
|
|
errorWidget: (context, url, error) {
|
|
|
|
// won't work on web because the image is downloaded by
|
|
|
|
// the cache manager instead
|
|
|
|
return buildPlaceholder();
|
|
|
|
},
|
|
|
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
|
|
|
);
|
|
|
|
if (heroKey != null) {
|
|
|
|
child = Hero(
|
|
|
|
tag: heroKey!,
|
|
|
|
child: child,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-22 20:05:31 +02:00
|
|
|
return Padding(
|
2022-01-15 11:35:15 +01:00
|
|
|
padding: padding,
|
2021-08-22 20:05:31 +02:00
|
|
|
child: FittedBox(
|
|
|
|
clipBehavior: Clip.hardEdge,
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
2021-06-22 07:24:37 +02:00
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
2021-08-22 20:05:31 +02:00
|
|
|
color: AppTheme.getListItemBackgroundColor(context),
|
2022-09-07 11:37:50 +02:00
|
|
|
child: child,
|
2021-06-22 07:24:37 +02:00
|
|
|
),
|
2021-08-22 20:05:31 +02:00
|
|
|
if (isGif)
|
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
alignment: AlignmentDirectional.topEnd,
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
|
|
child: const Icon(
|
|
|
|
Icons.gif,
|
|
|
|
size: 36,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
2022-01-25 11:15:17 +01:00
|
|
|
if (isFavorite)
|
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
alignment: AlignmentDirectional.bottomStart,
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
child: const Icon(
|
|
|
|
Icons.star,
|
|
|
|
size: 20,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
2021-08-22 20:05:31 +02:00
|
|
|
],
|
|
|
|
),
|
2021-05-06 13:29:20 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final Account account;
|
2022-01-15 11:35:15 +01:00
|
|
|
final String? previewUrl;
|
2021-06-22 07:24:37 +02:00
|
|
|
final bool isGif;
|
2022-01-15 11:35:15 +01:00
|
|
|
final EdgeInsetsGeometry padding;
|
2022-01-25 11:15:17 +01:00
|
|
|
final bool isFavorite;
|
2022-09-07 11:37:50 +02:00
|
|
|
// if not null, the image will be contained by a Hero widget
|
|
|
|
final String? heroKey;
|
2021-05-06 13:29:20 +02:00
|
|
|
}
|
2021-05-06 13:36:20 +02:00
|
|
|
|
|
|
|
class PhotoListVideo extends StatelessWidget {
|
|
|
|
const PhotoListVideo({
|
2021-07-23 22:05:57 +02:00
|
|
|
Key? key,
|
|
|
|
required this.account,
|
|
|
|
required this.previewUrl,
|
2022-01-25 11:15:17 +01:00
|
|
|
this.isFavorite = false,
|
2021-05-06 13:36:20 +02:00
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
2021-08-22 20:05:31 +02:00
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(2),
|
|
|
|
child: FittedBox(
|
|
|
|
clipBehavior: Clip.hardEdge,
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
color: AppTheme.getListItemBackgroundColor(context),
|
|
|
|
child: CachedNetworkImage(
|
2021-09-16 12:10:50 +02:00
|
|
|
cacheManager: ThumbnailCacheManager.inst,
|
2021-08-22 20:05:31 +02:00
|
|
|
imageUrl: previewUrl,
|
|
|
|
httpHeaders: {
|
|
|
|
"Authorization": Api.getAuthorizationHeaderValue(account),
|
|
|
|
},
|
|
|
|
fadeInDuration: const Duration(),
|
|
|
|
filterQuality: FilterQuality.high,
|
|
|
|
errorWidget: (context, url, error) {
|
|
|
|
// no preview for this video. Normal since video preview is disabled
|
|
|
|
// by default
|
2021-09-15 12:50:51 +02:00
|
|
|
return Center(
|
|
|
|
child: Icon(
|
|
|
|
Icons.image_not_supported,
|
|
|
|
size: 64,
|
|
|
|
color: Colors.white.withOpacity(.8),
|
2021-06-22 07:23:28 +02:00
|
|
|
),
|
2021-08-22 20:05:31 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
|
|
|
),
|
2021-06-22 07:23:28 +02:00
|
|
|
),
|
2021-08-22 20:05:31 +02:00
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
alignment: AlignmentDirectional.topEnd,
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
child: const Icon(
|
|
|
|
Icons.play_circle_outlined,
|
|
|
|
size: 24,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
2021-06-22 07:23:28 +02:00
|
|
|
),
|
2022-01-25 11:15:17 +01:00
|
|
|
if (isFavorite)
|
|
|
|
Container(
|
|
|
|
// arbitrary size here
|
|
|
|
constraints: BoxConstraints.tight(const Size(128, 128)),
|
|
|
|
alignment: AlignmentDirectional.bottomStart,
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
child: const Icon(
|
|
|
|
Icons.star,
|
|
|
|
size: 20,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
2021-08-22 20:05:31 +02:00
|
|
|
],
|
|
|
|
),
|
2021-05-06 13:36:20 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final Account account;
|
|
|
|
final String previewUrl;
|
2022-01-25 11:15:17 +01:00
|
|
|
final bool isFavorite;
|
2021-05-06 13:36:20 +02:00
|
|
|
}
|
2021-08-10 12:10:22 +02:00
|
|
|
|
|
|
|
class PhotoListLabel extends StatelessWidget {
|
|
|
|
const PhotoListLabel({
|
|
|
|
Key? key,
|
|
|
|
required this.text,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
|
|
|
return Container(
|
|
|
|
alignment: AlignmentDirectional.centerStart,
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
child: Text(
|
|
|
|
text,
|
|
|
|
style: Theme.of(context).textTheme.subtitle1,
|
|
|
|
maxLines: 2,
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final String text;
|
|
|
|
}
|
|
|
|
|
|
|
|
class PhotoListLabelEdit extends PhotoListLabel {
|
|
|
|
const PhotoListLabelEdit({
|
|
|
|
Key? key,
|
|
|
|
required String text,
|
|
|
|
required this.onEditPressed,
|
|
|
|
}) : super(key: key, text: text);
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
|
|
|
return Stack(
|
|
|
|
children: [
|
|
|
|
// needed to expand the touch sensitive area to the whole row
|
|
|
|
Container(
|
|
|
|
color: Colors.transparent,
|
|
|
|
),
|
|
|
|
super.build(context),
|
|
|
|
PositionedDirectional(
|
|
|
|
top: 0,
|
|
|
|
bottom: 0,
|
|
|
|
end: 0,
|
|
|
|
child: IconButton(
|
2021-09-15 08:58:06 +02:00
|
|
|
icon: const Icon(Icons.edit),
|
2021-08-29 13:51:43 +02:00
|
|
|
tooltip: L10n.global().editTooltip,
|
2021-08-10 12:10:22 +02:00
|
|
|
onPressed: onEditPressed,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final VoidCallback? onEditPressed;
|
|
|
|
}
|
2021-10-04 15:53:03 +02:00
|
|
|
|
|
|
|
class PhotoListDate extends StatelessWidget {
|
|
|
|
const PhotoListDate({
|
|
|
|
Key? key,
|
|
|
|
required this.date,
|
|
|
|
this.isMonthOnly = false,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
|
|
|
final pattern =
|
|
|
|
isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY;
|
|
|
|
final subtitle =
|
|
|
|
DateFormat(pattern, Localizations.localeOf(context).languageCode)
|
|
|
|
.format(date.toLocal());
|
|
|
|
return Align(
|
|
|
|
alignment: AlignmentDirectional.centerStart,
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
|
|
child: Text(
|
|
|
|
subtitle,
|
|
|
|
style: Theme.of(context).textTheme.caption!.copyWith(
|
|
|
|
color: AppTheme.getPrimaryTextColor(context),
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final DateTime date;
|
|
|
|
final bool isMonthOnly;
|
|
|
|
}
|