Attachment thumbnail

This commit is contained in:
LittleSheep 2024-09-10 22:47:28 +08:00
parent 8d9a8b5435
commit e385f79df2
3 changed files with 266 additions and 116 deletions

View File

@ -172,7 +172,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
end: Alignment.topCenter, end: Alignment.topCenter,
colors: [ colors: [
Theme.of(context).colorScheme.surface, Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface.withOpacity(0), Colors.transparent,
], ],
), ),
), ),

View File

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@ -221,19 +222,85 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const labelShadows = <Shadow>[
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(255, 0, 0, 0),
),
];
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9; final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
if (!_showContent) { if (!_showContent) {
return GestureDetector( return GestureDetector(
child: Column( child: Stack(
children: [ children: [
if (widget.item.metadata?['thumbnail'] != null) if (widget.item.metadata?['thumbnail'] != null)
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Image.network( child: AutoCacheImage(
ServiceFinder.buildUrl( ServiceFinder.buildUrl(
'uc', 'uc',
'/attachments/${widget.item.metadata?['thumbnail']}', '/attachments/${widget.item.metadata?['thumbnail']}',
), ),
fit: BoxFit.cover,
),
)
else
const Center(
child: Icon(Icons.movie, size: 64),
),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Colors.transparent,
],
),
),
),
),
),
Positioned(
bottom: 4,
left: 16,
right: 16,
child: SizedBox(
height: 45,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.item.alt,
style: const TextStyle(shadows: labelShadows),
),
Text(
Duration(
milliseconds:
(widget.item.metadata?['duration'] ?? 0) *
1000,
).toHumanReadableString(),
style: GoogleFonts.robotoMono(
fontSize: 12,
shadows: labelShadows,
),
),
],
),
),
const Icon(Icons.play_arrow, shadows: labelShadows)
.paddingOnly(bottom: 4, right: 8),
],
),
), ),
), ),
], ],
@ -302,6 +369,25 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
); );
} }
String _formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -312,38 +398,84 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const labelShadows = <Shadow>[
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(255, 0, 0, 0),
),
];
const ratio = 16 / 9; const ratio = 16 / 9;
if (!_showContent) { if (!_showContent) {
return GestureDetector( return GestureDetector(
child: AspectRatio( child: Stack(
aspectRatio: ratio,
child: CenteredContainer(
maxWidth: 280,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( if (widget.item.metadata?['thumbnail'] != null)
Icons.not_started, AspectRatio(
color: Colors.white, aspectRatio: 16 / 9,
size: 32, child: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${widget.item.metadata?['thumbnail']}',
), ),
const Gap(8), fit: BoxFit.cover,
),
)
else
const Center(
child: Icon(Icons.radio, size: 64),
),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Colors.transparent,
],
),
),
),
),
),
Positioned(
bottom: 4,
left: 16,
right: 16,
child: SizedBox(
height: 45,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
'attachmentUnload'.tr, widget.item.alt,
style: const TextStyle( style: const TextStyle(shadows: labelShadows),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
), ),
Text( Text(
'attachmentUnloadCaption'.tr, _formatBytes(widget.item.size),
style: const TextStyle(color: Colors.white), style: GoogleFonts.robotoMono(
textAlign: TextAlign.center, fontSize: 12,
shadows: labelShadows,
),
), ),
], ],
), ),
), ),
const Icon(Icons.play_arrow, shadows: labelShadows)
.paddingOnly(bottom: 4, right: 8),
],
),
),
),
],
), ),
onTap: () { onTap: () {
_startLoad(); _startLoad();
@ -355,7 +487,24 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
); );
} }
return AspectRatio( return Stack(
children: [
if (widget.item.metadata?['thumbnail'] != null)
AspectRatio(
aspectRatio: 16 / 9,
child: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${widget.item.metadata?['thumbnail']}',
),
fit: BoxFit.cover,
),
).animate().blur(
duration: 300.ms,
end: const Offset(10, 10),
curve: Curves.easeInOut,
),
AspectRatio(
aspectRatio: ratio, aspectRatio: ratio,
child: CenteredContainer( child: CenteredContainer(
maxWidth: 320, maxWidth: 320,
@ -385,24 +534,29 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
overlayShape: SliderComponentShape.noOverlay, overlayShape: SliderComponentShape.noOverlay,
), ),
child: Slider( child: Slider(
secondaryTrackValue: secondaryTrackValue: _bufferedPosition
_bufferedPosition.inMilliseconds.abs().toDouble(), .inMilliseconds
.abs()
.toDouble(),
value: _draggingValue?.abs() ?? value: _draggingValue?.abs() ??
_position.inMilliseconds.toDouble().abs(), _position.inMilliseconds.toDouble().abs(),
min: 0, min: 0,
max: max( max: math
.max(
_bufferedPosition.inMilliseconds.abs(), _bufferedPosition.inMilliseconds.abs(),
max( math.max(
_position.inMilliseconds.abs(), _position.inMilliseconds.abs(),
_duration.inMilliseconds.abs(), _duration.inMilliseconds.abs(),
), ),
).toDouble(), )
.toDouble(),
onChanged: (value) { onChanged: (value) {
setState(() => _draggingValue = value); setState(() => _draggingValue = value);
}, },
onChangeEnd: (value) { onChangeEnd: (value) {
_audioPlayer! _audioPlayer!.seek(
.seek(Duration(milliseconds: value.toInt())); Duration(milliseconds: value.toInt()),
);
setState(() => _draggingValue = null); setState(() => _draggingValue = null);
}, },
), ),
@ -441,6 +595,8 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
], ],
), ),
), ),
),
],
); );
} }

View File

@ -368,10 +368,7 @@ class _PostItemState extends State<PostItem> {
end: Alignment.topCenter, end: Alignment.topCenter,
colors: [ colors: [
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
Theme.of(context) Colors.transparent,
.colorScheme
.surface
.withOpacity(0),
], ],
), ),
), ),
@ -464,10 +461,7 @@ class _PostItemState extends State<PostItem> {
end: Alignment.topCenter, end: Alignment.topCenter,
colors: [ colors: [
Theme.of(context).colorScheme.surface, Theme.of(context).colorScheme.surface,
Theme.of(context) Colors.transparent,
.colorScheme
.surface
.withOpacity(0),
], ],
), ),
), ),