🐛 Fix share via image errored

This commit is contained in:
LittleSheep 2025-03-06 22:46:02 +08:00
parent 115cb4adc1
commit 1c510d63fe
4 changed files with 165 additions and 84 deletions

View File

@ -22,12 +22,14 @@ class AttachmentItem extends StatelessWidget {
final SnAttachment? data; final SnAttachment? data;
final String? heroTag; final String? heroTag;
final BoxFit fit; final BoxFit fit;
final FilterQuality? filterQuality;
const AttachmentItem({ const AttachmentItem({
super.key, super.key,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
required this.data, required this.data,
required this.heroTag, required this.heroTag,
this.filterQuality,
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
@ -47,6 +49,7 @@ class AttachmentItem extends StatelessWidget {
sn.getAttachmentUrl(data!.rid), sn.getAttachmentUrl(data!.rid),
key: Key('attachment-${data!.rid}-$tag'), key: Key('attachment-${data!.rid}-$tag'),
fit: fit, fit: fit,
filterQuality: filterQuality,
), ),
); );
case 'video': case 'video':
@ -83,13 +86,16 @@ class _AttachmentItemSensitiveBlur extends StatefulWidget {
final Widget child; final Widget child;
final bool isCompact; final bool isCompact;
const _AttachmentItemSensitiveBlur({required this.child, this.isCompact = false}); const _AttachmentItemSensitiveBlur(
{required this.child, this.isCompact = false});
@override @override
State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState(); State<_AttachmentItemSensitiveBlur> createState() =>
_AttachmentItemSensitiveBlurState();
} }
class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> { class _AttachmentItemSensitiveBlurState
extends State<_AttachmentItemSensitiveBlur> {
bool _doesShow = false; bool _doesShow = false;
@override @override
@ -124,10 +130,15 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
Text( Text(
'sensitiveContentDescription', 'sensitiveContentDescription',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)), )
.tr()
.fontSize(14)
.textColor(Colors.white.withOpacity(0.8)),
if (!widget.isCompact) const Gap(16), if (!widget.isCompact) const Gap(16),
InkWell( InkWell(
child: Text('sensitiveContentReveal').tr().textColor(Colors.white), child: Text('sensitiveContentReveal')
.tr()
.textColor(Colors.white),
onTap: () { onTap: () {
setState(() => _doesShow = !_doesShow); setState(() => _doesShow = !_doesShow);
}, },
@ -137,7 +148,9 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
).center(), ).center(),
), ),
), ),
).opacity(_doesShow ? 0 : 1, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut), )
.opacity(_doesShow ? 0 : 1, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
if (_doesShow) if (_doesShow)
Positioned( Positioned(
top: 0, top: 0,
@ -174,10 +187,12 @@ class _AttachmentItemContentVideo extends StatefulWidget {
}); });
@override @override
State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState(); State<_AttachmentItemContentVideo> createState() =>
_AttachmentItemContentVideoState();
} }
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> { class _AttachmentItemContentVideoState
extends State<_AttachmentItemContentVideo> {
bool _showContent = false; bool _showContent = false;
bool _showOriginal = false; bool _showOriginal = false;
@ -188,7 +203,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
setState(() => _showContent = true); setState(() => _showContent = true);
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid); final url = _showOriginal
? sn.getAttachmentUrl(widget.data.rid)
: sn.getAttachmentUrl(widget.data.compressed!.rid);
_videoPlayer = Player(); _videoPlayer = Player();
_videoController = VideoController(_videoPlayer!); _videoController = VideoController(_videoPlayer!);
_videoPlayer!.open(Media(url), play: !widget.isAutoload); _videoPlayer!.open(Media(url), play: !widget.isAutoload);
@ -201,7 +218,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
_videoPlayer?.open( _videoPlayer?.open(
Media( Media(
_showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid), _showOriginal
? sn.getAttachmentUrl(widget.data.rid)
: sn.getAttachmentUrl(widget.data.compressed!.rid),
), ),
play: true, play: true,
); );
@ -283,7 +302,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
), ),
Text( Text(
Duration( Duration(
milliseconds: (widget.data.data['duration'] ?? 0).toInt() * 1000, milliseconds:
(widget.data.data['duration'] ?? 0).toInt() *
1000,
).toString(), ).toString(),
style: GoogleFonts.robotoMono( style: GoogleFonts.robotoMono(
fontSize: 12, fontSize: 12,
@ -346,7 +367,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
MaterialDesktopCustomButton( MaterialDesktopCustomButton(
iconSize: 24, iconSize: 24,
onPressed: _toggleOriginal, onPressed: _toggleOriginal,
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24), icon: _showOriginal
? const Icon(Symbols.high_quality, size: 24)
: const Icon(Symbols.sd, size: 24),
), ),
], ],
), ),
@ -354,8 +377,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
child: Video( child: Video(
controller: _videoController!, controller: _videoController!,
aspectRatio: ratio, aspectRatio: ratio,
controls: controls: !kIsWeb && (Platform.isAndroid || Platform.isIOS)
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls, ? MaterialVideoControls
: MaterialDesktopVideoControls,
), ),
), ),
); );
@ -378,10 +402,12 @@ class _AttachmentItemContentAudio extends StatefulWidget {
}); });
@override @override
State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState(); State<_AttachmentItemContentAudio> createState() =>
_AttachmentItemContentAudioState();
} }
class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> { class _AttachmentItemContentAudioState
extends State<_AttachmentItemContentAudio> {
bool _showContent = false; bool _showContent = false;
double? _draggingValue; double? _draggingValue;
@ -552,8 +578,12 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
overlayShape: SliderComponentShape.noOverlay, overlayShape: SliderComponentShape.noOverlay,
), ),
child: Slider( child: Slider(
secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(), secondaryTrackValue: _bufferedPosition
value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(), .inMilliseconds
.abs()
.toDouble(),
value: _draggingValue?.abs() ??
_position.inMilliseconds.toDouble().abs(),
min: 0, min: 0,
max: math max: math
.max( .max(
@ -593,7 +623,9 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
), ),
const Gap(16), const Gap(16),
IconButton.filled( IconButton.filled(
icon: _isPlaying ? const Icon(Symbols.pause) : const Icon(Symbols.play_arrow), icon: _isPlaying
? const Icon(Symbols.pause)
: const Icon(Symbols.play_arrow),
onPressed: () { onPressed: () {
_audioPlayer!.playOrPause(); _audioPlayer!.playOrPause();
}, },

View File

@ -21,6 +21,7 @@ class AttachmentList extends StatefulWidget {
final double? minWidth; final double? minWidth;
final double? maxWidth; final double? maxWidth;
final EdgeInsets? padding; final EdgeInsets? padding;
final FilterQuality? filterQuality;
const AttachmentList({ const AttachmentList({
super.key, super.key,
@ -33,23 +34,27 @@ class AttachmentList extends StatefulWidget {
this.minWidth, this.minWidth,
this.maxWidth, this.maxWidth,
this.padding, this.padding,
this.filterQuality,
}); });
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8)); static const BorderRadius kDefaultRadius =
BorderRadius.all(Radius.circular(8));
@override @override
State<AttachmentList> createState() => _AttachmentListState(); State<AttachmentList> createState() => _AttachmentListState();
} }
class _AttachmentListState extends State<AttachmentList> { class _AttachmentListState extends State<AttachmentList> {
late final List<String> heroTags = List.generate(widget.data.length, (_) => const Uuid().v4()); late final List<String> heroTags =
List.generate(widget.data.length, (_) => const Uuid().v4());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, layoutConstraints) { builder: (context, layoutConstraints) {
final borderSide = final borderSide = widget.bordered
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none; ? BorderSide(width: 1, color: Theme.of(context).dividerColor)
: BorderSide.none;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer; final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final constraints = BoxConstraints( final constraints = BoxConstraints(
minWidth: widget.minWidth ?? 80, minWidth: widget.minWidth ?? 80,
@ -58,13 +63,13 @@ class _AttachmentListState extends State<AttachmentList> {
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) {
final singleAspectRatio = final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
widget.data[0]?.data['ratio']?.toDouble() ??
switch (widget.data[0]?.mimetype.split('/').firstOrNull) { switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
'audio' => 16 / 9, 'audio' => 16 / 9,
'video' => 16 / 9, 'video' => 16 / 9,
_ => 1, _ => 1,
}.toDouble(); }
.toDouble();
return Container( return Container(
padding: widget.padding ?? EdgeInsets.zero, padding: widget.padding ?? EdgeInsets.zero,
@ -80,12 +85,18 @@ class _AttachmentListState extends State<AttachmentList> {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(data: widget.data[0], heroTag: heroTags[0], fit: widget.fit), child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags[0],
fit: widget.fit,
filterQuality: widget.filterQuality,
),
), ),
), ),
), ),
onTap: () { onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return; if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
return;
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(), data: widget.data.where((ele) => ele != null).cast(),
@ -100,8 +111,10 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final fullOfImage = final fullOfImage = widget.data
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; .where((ele) => ele?.mediaType == SnMediaType.image)
.length ==
widget.data.length;
if (widget.gridded && fullOfImage) { if (widget.gridded && fullOfImage) {
return Container( return Container(
@ -117,29 +130,36 @@ class _AttachmentListState extends State<AttachmentList> {
crossAxisCount: math.min(widget.data.length, 2), crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4, crossAxisSpacing: 4,
mainAxisSpacing: 4, mainAxisSpacing: 4,
children: children: widget.data
widget.data .mapIndexed(
.mapIndexed( (idx, ele) => GestureDetector(
(idx, ele) => GestureDetector( child: Container(
child: Container( constraints: constraints,
constraints: constraints, child: AttachmentItem(
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover), data: ele,
), heroTag: heroTags[idx],
onTap: () { fit: BoxFit.cover,
if (widget.data[idx]!.mediaType != SnMediaType.image) return; filterQuality: widget.filterQuality,
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
), ),
) ),
.toList(), onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image)
return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data
.where((ele) => ele != null)
.cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
),
)
.toList(),
), ),
), ),
); );
@ -156,22 +176,26 @@ class _AttachmentListState extends State<AttachmentList> {
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
child: Column( child: Column(
children: children: widget.data
widget.data .mapIndexed(
.mapIndexed( (idx, ele) => GestureDetector(
(idx, ele) => GestureDetector( child: AspectRatio(
child: AspectRatio( aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1, child: Container(
child: Container( constraints: constraints,
constraints: constraints, child: AttachmentItem(
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover), data: ele,
), heroTag: heroTags[idx],
fit: BoxFit.cover,
filterQuality: widget.filterQuality,
), ),
), ),
) ),
.expand((ele) => [ele, const Divider(height: 1)]) ),
.toList() )
..removeLast(), .expand((ele) => [ele, const Divider(height: 1)])
.toList()
..removeLast(),
), ),
), ),
); );
@ -189,16 +213,22 @@ class _AttachmentListState extends State<AttachmentList> {
itemCount: widget.data.length, itemCount: widget.data.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return Container( return Container(
constraints: constraints.copyWith(maxWidth: widget.maxWidth), constraints:
constraints.copyWith(maxWidth: widget.maxWidth),
child: AspectRatio( child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), aspectRatio:
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return; if (widget.data[idx]?.mediaType != SnMediaType.image)
return;
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: data: widget.data
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), .where((ele) =>
ele != null &&
ele.mediaType == SnMediaType.image)
.cast(),
initialIndex: idx, initialIndex: idx,
heroTags: heroTags, heroTags: heroTags,
), ),
@ -212,18 +242,25 @@ class _AttachmentListState extends State<AttachmentList> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
border: Border(top: borderSide, bottom: borderSide), border:
Border(top: borderSide, bottom: borderSide),
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(data: widget.data[idx], heroTag: heroTags[idx]), child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
filterQuality: widget.filterQuality,
),
), ),
), ),
Positioned( Positioned(
right: 8, right: 8,
bottom: 8, bottom: 8,
child: Chip(label: Text('${idx + 1}/${widget.data.length}')), child: Chip(
label:
Text('${idx + 1}/${widget.data.length}')),
), ),
], ],
), ),
@ -245,5 +282,6 @@ class _AttachmentListState extends State<AttachmentList> {
class _AttachmentListScrollBehavior extends MaterialScrollBehavior { class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
@override @override
Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse}; Set<PointerDeviceKind> get dragDevices =>
{PointerDeviceKind.touch, PointerDeviceKind.mouse};
} }

View File

@ -149,7 +149,6 @@ class PostItem extends StatelessWidget {
void _doShareViaPicture(BuildContext context) async { void _doShareViaPicture(BuildContext context) async {
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
context.showSnackbar('postSharingViaPicture'.tr());
final controller = ScreenshotController(); final controller = ScreenshotController();
final capturedImage = await controller.captureFromLongWidget( final capturedImage = await controller.captureFromLongWidget(
@ -160,9 +159,9 @@ class PostItem extends StatelessWidget {
child: Material( child: Material(
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
// Create a copy of environments
Provider<SnNetworkProvider>(create: (_) => context.read()), Provider<SnNetworkProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>( Provider<UserDirectoryProvider>(create: (_) => context.read()),
create: (_) => context.read()),
], ],
child: ResponsiveBreakpoints.builder( child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints, breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
@ -507,6 +506,8 @@ class PostShareImageWidget extends StatelessWidget {
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
columned: true, columned: true,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
)).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -34,11 +34,14 @@ class UniversalImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null; final double? resizeHeight =
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null; cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth =
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
return Image( return Image(
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality, filterQuality:
filterQuality ?? context.read<ConfigProvider>().imageQuality,
image: kIsWeb image: kIsWeb
? UniversalImage.provider(url) ? UniversalImage.provider(url)
: ResizeImage( : ResizeImage(
@ -52,7 +55,8 @@ class UniversalImage extends StatelessWidget {
fit: fit, fit: fit,
loadingBuilder: noProgressIndicator loadingBuilder: noProgressIndicator
? null ? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { : (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Container( return Container(
constraints: BoxConstraints(maxHeight: 80), constraints: BoxConstraints(maxHeight: 80),
@ -61,12 +65,15 @@ class UniversalImage extends StatelessWidget {
tween: Tween( tween: Tween(
begin: 0, begin: 0,
end: loadingProgress.expectedTotalBytes != null end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! ? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: 0, : 0,
), ),
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator( builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, value: loadingProgress.expectedTotalBytes != null
? value.toDouble()
: null,
), ),
), ),
), ),
@ -114,6 +121,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
final BoxFit? fit; final BoxFit? fit;
final bool noProgressIndicator; final bool noProgressIndicator;
final bool noErrorWidget; final bool noErrorWidget;
final FilterQuality? filterQuality;
const AutoResizeUniversalImage( const AutoResizeUniversalImage(
this.url, { this.url, {
@ -123,6 +131,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
this.fit, this.fit,
this.noProgressIndicator = false, this.noProgressIndicator = false,
this.noErrorWidget = false, this.noErrorWidget = false,
this.filterQuality,
}); });
@override @override
@ -137,6 +146,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
noErrorWidget: noErrorWidget, noErrorWidget: noErrorWidget,
cacheHeight: constraints.maxHeight, cacheHeight: constraints.maxHeight,
cacheWidth: constraints.maxWidth, cacheWidth: constraints.maxWidth,
filterQuality: filterQuality,
); );
}); });
} }