♻️ Better attachment list & zoom view
This commit is contained in:
parent
9588fc0475
commit
b0790ea145
@ -351,5 +351,7 @@
|
|||||||
"friendRequestAccept": "Accept",
|
"friendRequestAccept": "Accept",
|
||||||
"friendRequestDecline": "Decline",
|
"friendRequestDecline": "Decline",
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
"unsubscribe": "Unsubscribe"
|
"unsubscribe": "Unsubscribe",
|
||||||
|
"attachmentUploadBy": "Upload by",
|
||||||
|
"attachmentShotOn": "Shot on {}"
|
||||||
}
|
}
|
||||||
|
@ -351,5 +351,7 @@
|
|||||||
"friendRequestAccept": "接受",
|
"friendRequestAccept": "接受",
|
||||||
"friendRequestDecline": "拒绝",
|
"friendRequestDecline": "拒绝",
|
||||||
"subscribe": "订阅",
|
"subscribe": "订阅",
|
||||||
"unsubscribe": "取消订阅"
|
"unsubscribe": "取消订阅",
|
||||||
|
"attachmentUploadBy": "上传者",
|
||||||
|
"attachmentShotOn": "由 {} 拍摄"
|
||||||
}
|
}
|
||||||
|
@ -81,9 +81,9 @@ class SolianApp extends StatelessWidget {
|
|||||||
|
|
||||||
// Data layer
|
// Data layer
|
||||||
Provider(create: (_) => SnNetworkProvider()),
|
Provider(create: (_) => SnNetworkProvider()),
|
||||||
|
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||||
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
|
|
||||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||||
|
@ -2,14 +2,17 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
|
|
||||||
class SnPostContentProvider {
|
class SnPostContentProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final UserDirectoryProvider _ud;
|
||||||
late final SnAttachmentProvider _attach;
|
late final SnAttachmentProvider _attach;
|
||||||
|
|
||||||
SnPostContentProvider(BuildContext context) {
|
SnPostContentProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_attach = context.read<SnAttachmentProvider>();
|
_attach = context.read<SnAttachmentProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +40,13 @@ class SnPostContentProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _ud.listAccount(
|
||||||
|
attachments
|
||||||
|
.where((ele) => ele != null)
|
||||||
|
.map((ele) => ele!.accountId)
|
||||||
|
.toSet(),
|
||||||
|
);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:photo_view/photo_view_gallery.dart';
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@ -27,17 +33,37 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
late final PageController _pageController =
|
late final PageController _pageController =
|
||||||
PageController(initialPage: widget.initialIndex ?? 0);
|
PageController(initialPage: widget.initialIndex ?? 0);
|
||||||
|
|
||||||
|
void _updatePage() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_pageController.addListener(_updatePage);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_pageController.removeListener(_updatePage);
|
||||||
_pageController.dispose();
|
_pageController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final uuid = Uuid();
|
final uuid = Uuid();
|
||||||
|
|
||||||
|
final metaTextStyle = GoogleFonts.roboto(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _unFocusColor,
|
||||||
|
height: 1,
|
||||||
|
);
|
||||||
|
|
||||||
return DismissiblePage(
|
return DismissiblePage(
|
||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -45,14 +71,18 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
direction: DismissiblePageDismissDirection.down,
|
direction: DismissiblePageDismissDirection.down,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
child: Builder(builder: (context) {
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Builder(builder: (context) {
|
||||||
if (widget.data.length == 1) {
|
if (widget.data.length == 1) {
|
||||||
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
child: PhotoView(
|
child: PhotoView(
|
||||||
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
key: Key(
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
'attachment-detail-${widget.data.first.rid}-$heroTag'),
|
||||||
|
backgroundDecoration:
|
||||||
|
BoxDecoration(color: Colors.transparent),
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.first.rid),
|
sn.getAttachmentUrl(widget.data.first.rid),
|
||||||
),
|
),
|
||||||
@ -90,6 +120,163 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
height: 300,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
Theme.of(context).colorScheme.surface,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom > 16
|
||||||
|
? -MediaQuery.of(context).padding.bottom
|
||||||
|
: 16,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 180,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
final item = widget.data.elementAt(
|
||||||
|
_pageController.page?.round() ?? 0,
|
||||||
|
);
|
||||||
|
final account = ud.getAccountFromCache(item.accountId);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (item.accountId > 0)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IgnorePointer(
|
||||||
|
child: AccountImage(
|
||||||
|
content: account!.avatar,
|
||||||
|
radius: 19,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'attachmentUploadBy'.tr(),
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
account.nick,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.data.length > 1)
|
||||||
|
IgnorePointer(
|
||||||
|
child: Text(
|
||||||
|
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
).padding(right: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Text(
|
||||||
|
item.alt,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(2),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
if (item.metadata['exif'] == null)
|
||||||
|
Text(
|
||||||
|
'#${item.rid}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'attachmentShotOn'.tr(args: [
|
||||||
|
item.metadata['exif']?['Model'],
|
||||||
|
]),
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['ShutterSpeed'] != null)
|
||||||
|
Text(
|
||||||
|
item.metadata['exif']?['ShutterSpeed'],
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['ISO'] != null)
|
||||||
|
Text(
|
||||||
|
'ISO${item.metadata['exif']?['ISO']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['Aperture'] != null)
|
||||||
|
Text(
|
||||||
|
'f/${item.metadata['exif']?['Aperture']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['Megapixels'] != null &&
|
||||||
|
item.metadata['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'${item.metadata['exif']?['Megapixels']}MP',
|
||||||
|
style: metaTextStyle,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'${item.size} Bytes',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${item.metadata['width']}x${item.metadata['height']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['ratio'] != null)
|
||||||
|
Text(
|
||||||
|
(item.metadata['ratio'] as num)
|
||||||
|
.toStringAsFixed(2),
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
item.mimetype,
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,18 +38,30 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final aspectRatio = widget.data[0]?.metadata['ratio']?.toDouble() ?? 1;
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, layoutConstraints) {
|
||||||
final borderSide = widget.bordered
|
final borderSide = widget.bordered
|
||||||
? BorderSide(width: 1, color: Theme.of(context).dividerColor)
|
? BorderSide(width: 1, color: Theme.of(context).dividerColor)
|
||||||
: BorderSide.none;
|
: BorderSide.none;
|
||||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||||
final constraints = BoxConstraints(
|
final constraints = BoxConstraints(
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
|
maxWidth: layoutConstraints.maxWidth - 20,
|
||||||
maxHeight: widget.maxHeight ?? double.infinity,
|
maxHeight: widget.maxHeight ?? double.infinity,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||||
if (widget.data.length == 1) {
|
if (widget.data.length == 1) {
|
||||||
return GestureDetector(
|
return AspectRatio(
|
||||||
|
aspectRatio: widget.data[0]?.metadata['ratio']?.toDouble() ??
|
||||||
|
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
|
||||||
|
'audio' => 16 / 9,
|
||||||
|
'video' => 16 / 9,
|
||||||
|
_ => 1,
|
||||||
|
},
|
||||||
|
child: GestureDetector(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) ||
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) ||
|
||||||
@ -64,15 +76,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
border: Border(top: borderSide, bottom: borderSide),
|
border: Border(top: borderSide, bottom: borderSide),
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: widget.data[0]?.metadata['ratio']
|
|
||||||
?.toDouble() ??
|
|
||||||
switch (
|
|
||||||
widget.data[0]?.mimetype.split('/').firstOrNull) {
|
|
||||||
'audio' => 16 / 9,
|
|
||||||
'video' => 16 / 9,
|
|
||||||
_ => 1,
|
|
||||||
},
|
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
@ -81,7 +84,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,13 +92,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
border: Border(top: borderSide, bottom: borderSide),
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: widget.data[0]?.metadata['ratio']?.toDouble() ?? 1,
|
|
||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
data: widget.data[0],
|
data: widget.data[0],
|
||||||
heroTag: heroTags.first,
|
heroTag: heroTags.first,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -111,10 +110,13 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return AspectRatio(
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
child: Container(
|
||||||
constraints: BoxConstraints(maxHeight: widget.maxHeight ?? 320),
|
constraints: BoxConstraints(maxHeight: widget.maxHeight ?? 320),
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: _AttachmentListScrollBehavior(),
|
behavior: _AttachmentListScrollBehavior(),
|
||||||
@ -143,9 +145,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
border: Border(top: borderSide, bottom: borderSide),
|
border: Border(top: borderSide, bottom: borderSide),
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio:
|
|
||||||
widget.data[idx]?.metadata['ratio']?.toDouble() ?? 1,
|
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
@ -154,9 +153,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 12,
|
right: 8,
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
child: Chip(
|
child: Chip(
|
||||||
label: Text('${idx + 1}/${widget.data.length}'),
|
label: Text('${idx + 1}/${widget.data.length}'),
|
||||||
@ -172,6 +170,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user