Compare commits

...

8 Commits

Author SHA1 Message Date
133213b430 🚑 Hot fix thumbnail duration issue 2024-09-10 23:52:10 +08:00
2ff142a84f 🚀 Launch 1.2.1+33 2024-09-10 23:46:08 +08:00
e385f79df2 Attachment thumbnail 2024-09-10 22:47:28 +08:00
8d9a8b5435 🧑‍💻 All in one auto cache image widget 2024-09-10 21:53:05 +08:00
c5a975b5ed Setting of attachment thumbnail 2024-09-10 21:36:10 +08:00
1210cda998 Roll back to use media_kit as media player 2024-09-09 23:09:16 +08:00
3b56b94242 🚀 Launch 1.2.1+30 2024-09-09 13:07:21 +08:00
34043e722b Able to play music 2024-09-08 22:43:01 +08:00
21 changed files with 802 additions and 274 deletions

View File

@ -22,7 +22,8 @@
android:label="Solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true">
android:supportsRtl="true"
android:usesCleartextTraffic="true">
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"

View File

@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:media_kit/media_kit.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart';
import 'package:solian/bootstrapper.dart';
@ -35,7 +34,6 @@ import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
await Future.wait([
_initializeFirebase(),

View File

@ -192,6 +192,7 @@ class AttachmentProvider extends GetConnect {
Future<Response> updateAttachment(
int id,
String alt, {
required Map<String, dynamic> metadata,
bool isMature = false,
}) async {
final AuthProvider auth = Get.find();
@ -201,6 +202,7 @@ class AttachmentProvider extends GetConnect {
var resp = await client.put('/attachments/$id', {
'alt': alt,
'metadata': metadata,
'is_mature': isMature,
});

View File

@ -0,0 +1,26 @@
extension DurationToHumanReadableString on Duration {
String toHumanReadableString({padZero = true}) {
final mm = inMinutes
.remainder(60)
.toString()
.padLeft(2, !padZero && inHours == 0 ? '' : '0');
final ss = inSeconds.remainder(60).toString().padLeft(2, '0');
if (inHours > 0) {
final hh = inHours.toString().padLeft(2, !padZero ? '' : '0');
return '$hh:$mm:$ss';
}
return '$mm:$ss';
}
}
extension ParseDuration on Duration {
static Duration fromString(String duration) {
final parts = duration.split(':').reversed.toList();
final seconds = int.parse(parts[0]);
final minutes = parts.length > 1 ? int.parse(parts[1]) : 0;
final hours = parts.length > 2 ? int.parse(parts[2]) : 0;
return Duration(hours: hours, minutes: minutes, seconds: seconds);
}
}

View File

@ -1,14 +1,13 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/stickers/sticker_uploader.dart';
class StickerScreen extends StatefulWidget {
@ -94,16 +93,11 @@ class _StickerScreenState extends State<StickerScreen> {
),
],
),
leading: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: imageUrl,
width: 28,
height: 28,
)
: Image.network(
leading: AutoCacheImage(
imageUrl,
width: 28,
height: 28,
noErrorWidget: true,
),
);
}

View File

@ -177,7 +177,9 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: Row(
title: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Text(
_editorController.title ?? 'title'.tr,
@ -187,10 +189,12 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
const Gap(6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label: Text('#${_editorController.aliasController.text}'),
label:
Text('#${_editorController.aliasController.text}'),
),
],
),
),
subtitle: Text(
_editorController.description ?? 'description'.tr,
maxLines: 2,

View File

@ -1,7 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:solian/platform.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.dart';
class AccountAvatar extends StatelessWidget {
final dynamic content;
@ -34,11 +33,7 @@ class AccountAvatar extends StatelessWidget {
key: Key('a$content'),
radius: radius,
backgroundColor: bgColor,
backgroundImage: !isEmpty
? (PlatformInfo.canCacheImage
? CachedNetworkImageProvider(url)
: NetworkImage(url)) as ImageProvider<Object>?
: null,
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
child: isEmpty
? Icon(
Icons.account_circle,
@ -74,33 +69,6 @@ class AccountProfileImage extends StatelessWidget {
? content
: ServiceFinder.buildUrl('files', '/attachments/$content');
if (PlatformInfo.canCacheImage) {
return CachedNetworkImage(
imageUrl: url,
fit: fit,
progressIndicatorBuilder: (context, url, downloadProgress) => Center(
child: CircularProgressIndicator(
value: downloadProgress.progress,
),
),
);
} else {
return Image.network(
url,
fit: fit,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
);
}
return AutoCacheImage(url, fit: fit, noErrorWidget: true);
}
}

View File

@ -37,6 +37,7 @@ class _AttachmentAttrEditorDialogState
widget.item.id,
_altController.value.text,
isMature: _isMature,
metadata: widget.item.metadata ?? {},
);
Get.find<AttachmentProvider>().clearCache(id: widget.item.rid);

View File

@ -20,6 +20,7 @@ import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
import 'package:solian/widgets/attachments/attachment_editor_thumbnail.dart';
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentEditorPopup extends StatefulWidget {
@ -264,6 +265,21 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
);
}
void _showAttachmentThumbnailEditor(Attachment element, int idx) {
showDialog(
context: context,
builder: (context) => AttachmentEditorThumbnailDialog(
item: element,
pool: widget.pool,
initialItem: element.metadata?['thumbnail'],
onUpdate: (value) {
_attachments[idx]!.metadata ??= {};
_attachments[idx]!.metadata!['thumbnail'] = value;
},
),
);
}
void _showEdit(Attachment element, int index) {
showDialog(
context: context,
@ -455,11 +471,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
);
}
Widget _buildListEntry(Attachment element, int index) {
Widget _buildListEntry(Attachment element, int idx) {
var fileType = element.mimetype.split('/').firstOrNull;
fileType ??= 'unknown';
final canBePreview = fileType.toLowerCase() == 'image';
final canHasThumbnail = fileType.toLowerCase() != 'image';
return Container(
padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16),
@ -491,13 +508,22 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
],
),
),
if (canBePreview)
IconButton(
color: Colors.teal,
icon: const Icon(Icons.preview),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: canBePreview
? () => _showAttachmentPreview(element)
: null,
onPressed: () => _showAttachmentPreview(element),
),
if (canHasThumbnail)
IconButton(
color: Colors.teal,
icon: const Icon(Icons.add_photo_alternate),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: () => _showAttachmentThumbnailEditor(
element,
idx,
),
),
PopupMenuButton(
icon: const Icon(Icons.more_horiz),
@ -514,7 +540,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
horizontal: 8,
),
),
onTap: () => _showEdit(element, index),
onTap: () => _showEdit(element, idx),
),
PopupMenuItem(
child: ListTile(
@ -527,7 +553,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
onTap: () {
_deleteAttachment(element).then((_) {
widget.onRemove(element.rid);
setState(() => _attachments.removeAt(index));
setState(() => _attachments.removeAt(idx));
});
},
),
@ -541,7 +567,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
),
onTap: () {
widget.onRemove(element.rid);
setState(() => _attachments.removeAt(index));
setState(() => _attachments.removeAt(idx));
},
),
],

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
class AttachmentEditorThumbnailDialog extends StatefulWidget {
final Attachment item;
final String pool;
final String? initialItem;
final Function(String? id) onUpdate;
const AttachmentEditorThumbnailDialog({
super.key,
required this.item,
required this.pool,
required this.initialItem,
required this.onUpdate,
});
@override
State<AttachmentEditorThumbnailDialog> createState() =>
_AttachmentEditorThumbnailDialogState();
}
class _AttachmentEditorThumbnailDialogState
extends State<AttachmentEditorThumbnailDialog> {
bool _isLoading = false;
final TextEditingController _attachmentController = TextEditingController();
void _promptUploadNewAttachment() {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
pool: widget.pool,
singleMode: true,
imageOnly: true,
autoUpload: true,
onAdd: (value) {
widget.onUpdate(value);
_attachmentController.text = value;
},
initialAttachments: const [],
onRemove: (_) {},
),
);
}
Future<void> _updateAttachment() async {
setState(() => _isLoading = true);
final AttachmentProvider attach = Get.find();
widget.item.metadata ??= {};
if (_attachmentController.text.isNotEmpty) {
widget.item.metadata!['thumbnail'] = _attachmentController.text;
} else {
widget.item.metadata!['thumbnail'] = null;
}
try {
await attach.updateAttachment(
widget.item.id,
widget.item.alt,
isMature: widget.item.isMature,
metadata: widget.item.metadata!,
);
Get.find<AttachmentProvider>().clearCache(id: widget.item.rid);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isLoading = false);
}
}
@override
void initState() {
if (widget.initialItem != null) {
_attachmentController.text = widget.initialItem!;
}
super.initState();
}
@override
void dispose() {
_attachmentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('postThumbnail'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Card(
margin: EdgeInsets.zero,
child: ListTile(
title: Text('postThumbnailAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 12, right: 9),
trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
),
),
const Row(children: <Widget>[
Expanded(child: Divider()),
Text('OR'),
Expanded(child: Divider()),
]).paddingOnly(top: 12, bottom: 16, left: 16, right: 16),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'postThumbnailAttachment'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isLoading
? null
: () {
_updateAttachment().then((_) {
widget.onUpdate(_attachmentController.text);
if (mounted) {
Navigator.pop(context);
}
});
},
child: Text('confirm'.tr),
),
],
);
}
}

View File

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

View File

@ -1,13 +1,16 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/durations.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -56,6 +59,11 @@ class _AttachmentItemState extends State<AttachmentItem> {
item: widget.item,
autoload: widget.autoload,
);
case 'audio':
return _AttachmentItemAudio(
item: widget.item,
autoload: widget.autoload,
);
default:
return Center(
child: Container(
@ -131,65 +139,12 @@ class _AttachmentItemImage extends StatelessWidget {
child: Stack(
fit: StackFit.expand,
children: [
if (PlatformInfo.canCacheImage)
CachedNetworkImage(
fit: fit,
imageUrl: ServiceFinder.buildUrl(
AutoCacheImage(
ServiceFinder.buildUrl(
'files',
'/attachments/${item.rid}',
),
progressIndicatorBuilder: (context, url, downloadProgress) {
return Center(
child: CircularProgressIndicator(
value: downloadProgress.progress,
),
);
},
errorWidget: (context, url, error) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(error.toString()),
],
),
);
},
)
else
Image.network(
ServiceFinder.buildUrl('files', '/attachments/${item.rid}'),
fit: fit,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(error.toString()),
],
),
);
},
),
if (showBadge && badge != null)
Positioned(
@ -240,22 +195,21 @@ class _AttachmentItemVideo extends StatefulWidget {
}
class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
late final _player = Player(
configuration: const PlayerConfiguration(
logLevel: MPVLogLevel.error,
),
);
late final _controller = VideoController(_player);
bool _showContent = false;
Player? _videoPlayer;
VideoController? _videoController;
Future<void> _startLoad() async {
await _player.open(
Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}')),
play: false,
);
setState(() => _showContent = true);
MediaKit.ensureInitialized();
final url = ServiceFinder.buildUrl(
'files',
'/attachments/${widget.item.rid}',
);
_videoPlayer = Player();
_videoController = VideoController(_videoPlayer!);
_videoPlayer!.open(Media(url), play: !widget.autoload);
}
@override
@ -268,54 +222,405 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
@override
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;
if (!_showContent) {
return GestureDetector(
child: AspectRatio(
aspectRatio: ratio,
child: CenteredContainer(
maxWidth: 280,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
child: Stack(
children: [
const Icon(
Icons.not_started,
color: Colors.white,
size: 32,
if (widget.item.metadata?['thumbnail'] != null)
AspectRatio(
aspectRatio: 16 / 9,
child: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${widget.item.metadata?['thumbnail']}',
),
const Gap(8),
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(
'attachmentUnload'.tr,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
widget.item.alt,
style: const TextStyle(shadows: labelShadows),
),
Text(
'attachmentUnloadCaption'.tr,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
Duration(
milliseconds:
(widget.item.metadata?['duration'] ?? 0)
.toInt() *
1000,
).toHumanReadableString(),
style: GoogleFonts.robotoMono(
fontSize: 12,
shadows: labelShadows,
),
),
],
),
),
const Icon(Icons.play_arrow, shadows: labelShadows)
.paddingOnly(bottom: 4, right: 8),
],
),
),
),
],
),
onTap: () {
_startLoad();
},
);
} else if (_videoController == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Video(
controller: _videoController!,
aspectRatio: ratio,
controller: _controller,
);
}
@override
void dispose() {
_player.dispose();
_videoPlayer?.dispose();
super.dispose();
}
}
class _AttachmentItemAudio extends StatefulWidget {
final Attachment item;
final bool autoload;
const _AttachmentItemAudio({
required this.item,
this.autoload = false,
});
@override
State<_AttachmentItemAudio> createState() => _AttachmentItemAudioState();
}
class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
bool _showContent = false;
double? _draggingValue;
bool _isPlaying = false;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
Duration _bufferedPosition = Duration.zero;
Player? _audioPlayer;
Future<void> _startLoad() async {
setState(() => _showContent = true);
MediaKit.ensureInitialized();
final url = ServiceFinder.buildUrl(
'files',
'/attachments/${widget.item.rid}',
);
_audioPlayer = Player();
await _audioPlayer!.open(Media(url), play: !widget.autoload);
_audioPlayer!.stream.playing.listen((v) => setState(() => _isPlaying = v));
_audioPlayer!.stream.position.listen((v) => setState(() => _position = v));
_audioPlayer!.stream.duration.listen((v) => setState(() => _duration = v));
_audioPlayer!.stream.buffer.listen(
(v) => setState(() => _bufferedPosition = v),
);
}
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
void initState() {
super.initState();
if (widget.autoload) {
_startLoad();
}
}
@override
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;
if (!_showContent) {
return GestureDetector(
child: 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,
),
)
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(
widget.item.alt,
style: const TextStyle(shadows: labelShadows),
),
Text(
_formatBytes(widget.item.size),
style: GoogleFonts.robotoMono(
fontSize: 12,
shadows: labelShadows,
),
),
],
),
),
const Icon(Icons.play_arrow, shadows: labelShadows)
.paddingOnly(bottom: 4, right: 8),
],
),
),
),
],
),
onTap: () {
_startLoad();
},
);
} else if (_audioPlayer == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
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,
child: CenteredContainer(
maxWidth: 320,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.audio_file, size: 32),
const Gap(8),
Text(
widget.item.alt,
style: const TextStyle(fontSize: 13),
textAlign: TextAlign.center,
),
const Gap(12),
Row(
children: [
Expanded(
child: Column(
children: [
SliderTheme(
data: SliderThemeData(
trackHeight: 2,
trackShape: _PlayerProgressTrackShape(),
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8,
),
overlayShape: SliderComponentShape.noOverlay,
),
child: Slider(
secondaryTrackValue: _bufferedPosition
.inMilliseconds
.abs()
.toDouble(),
value: _draggingValue?.abs() ??
_position.inMilliseconds.toDouble().abs(),
min: 0,
max: math
.max(
_bufferedPosition.inMilliseconds.abs(),
math.max(
_position.inMilliseconds.abs(),
_duration.inMilliseconds.abs(),
),
)
.toDouble(),
onChanged: (value) {
setState(() => _draggingValue = value);
},
onChangeEnd: (value) {
_audioPlayer!.seek(
Duration(milliseconds: value.toInt()),
);
setState(() => _draggingValue = null);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_position.toHumanReadableString(),
style: GoogleFonts.robotoMono(fontSize: 12),
),
Text(
_duration.toHumanReadableString(),
style: GoogleFonts.robotoMono(fontSize: 12),
),
],
).paddingSymmetric(horizontal: 8, vertical: 4),
],
),
),
const Gap(16),
IconButton.filled(
icon: _isPlaying
? const Icon(Icons.pause)
: const Icon(Icons.play_arrow),
onPressed: () {
_audioPlayer!.playOrPause();
},
visualDensity: const VisualDensity(
horizontal: -4,
vertical: 0,
),
),
],
),
],
),
),
),
],
);
}
@override
void dispose() {
_audioPlayer?.dispose();
super.dispose();
}
}
class _PlayerProgressTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final trackHeight = sliderTheme.trackHeight;
final trackLeft = offset.dx;
final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2;
final trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
}

View File

@ -94,6 +94,8 @@ class _AttachmentListState extends State<AttachmentList> {
} else {
portrait++;
}
} else if (entry.mimetype.split('/').firstOrNull == 'audio') {
landscape++;
}
}
if (isConsistent && consistentValue != null) {

View File

@ -0,0 +1,113 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:solian/platform.dart';
import 'package:solian/widgets/sized_container.dart';
class AutoCacheImage extends StatelessWidget {
final String url;
final double? width, height;
final BoxFit? fit;
final bool noProgressIndicator;
final bool noErrorWidget;
const AutoCacheImage(
this.url, {
super.key,
this.width,
this.height,
this.fit,
this.noProgressIndicator = false,
this.noErrorWidget = false,
});
@override
Widget build(BuildContext context) {
if (PlatformInfo.canCacheImage) {
return CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
fit: fit,
progressIndicatorBuilder: noProgressIndicator
? null
: (context, url, downloadProgress) => Center(
child: CircularProgressIndicator(
value: downloadProgress.progress,
),
),
errorWidget: noErrorWidget
? null
: (context, url, error) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: CenteredContainer(
maxWidth: 280,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
error.toString(),
textAlign: TextAlign.center,
),
],
),
),
);
},
);
}
return Image.network(
url,
width: width,
height: height,
fit: fit,
loadingBuilder: noProgressIndicator
? null
: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: noErrorWidget
? null
: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: CenteredContainer(
maxWidth: 280,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
error.toString(),
textAlign: TextAlign.center,
),
],
),
),
);
},
);
}
static ImageProvider provider(String url) {
if (PlatformInfo.canCacheImage) {
return CachedNetworkImageProvider(url);
}
return NetworkImage(url);
}
}

View File

@ -1,16 +1,15 @@
import 'dart:convert';
import 'package:avatar_stack/avatar_stack.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart';
class ChannelCallIndicator extends StatelessWidget {
@ -76,10 +75,7 @@ class ChannelCallIndicator extends StatelessWidget {
avatars: ongoingCall.participants!.map((x) {
final userinfo =
Account.fromJson(jsonDecode(x['metadata']));
return PlatformInfo.canCacheImage
? CachedNetworkImageProvider(userinfo.avatar)
as ImageProvider
: NetworkImage(userinfo.avatar) as ImageProvider;
return AutoCacheImage.provider(userinfo.avatar);
}).toList(),
),
);

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -11,13 +10,13 @@ import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/chat/chat_event.dart';
import 'package:badges/badges.dart' as badges;
import 'package:uuid/uuid.dart';
@ -414,13 +413,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
.map(
(x) => ChatMessageSuggestion(
type: 'emotes',
leading: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: x.imageUrl,
width: 28,
height: 28,
)
: Image.network(
leading: AutoCacheImage(
x.imageUrl,
width: 28,
height: 28,

View File

@ -1,11 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LinkExpansion extends StatelessWidget {
@ -17,36 +15,10 @@ class LinkExpansion extends StatelessWidget {
if (url.endsWith('svg')) {
return SvgPicture.network(url, width: width, height: height);
}
return PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
errorWidget: (context, url, error) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
),
);
},
)
: Image.network(
return AutoCacheImage(
url,
width: width,
height: height,
errorBuilder: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: const Icon(Icons.close, size: 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
),
);
},
);
}

View File

@ -1,12 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
import 'package:get/get.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown/markdown.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'account/account_profile_popup.dart';
@ -107,14 +106,7 @@ class MarkdownTextContent extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
fit: fit,
)
: Image.network(
child: AutoCacheImage(
url,
width: width,
height: height,
@ -138,14 +130,7 @@ class MarkdownTextContent extends StatelessWidget {
}
}
return PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
fit: fit,
)
: Image.network(
return AutoCacheImage(
url,
width: width,
height: height,

View File

@ -299,14 +299,14 @@ class _PostItemState extends State<PostItem> {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
autoload: false,
isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4);
} else if (attachments.length > 1) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
autoload: false,
isColumn: true,
).paddingOnly(left: 60, right: 24);
} else {
@ -314,7 +314,7 @@ class _PostItemState extends State<PostItem> {
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: true,
autoload: false,
);
}
}
@ -368,10 +368,7 @@ class _PostItemState extends State<PostItem> {
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surfaceContainerLow,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
Colors.transparent,
],
),
),
@ -464,10 +461,7 @@ class _PostItemState extends State<PostItem> {
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
Colors.transparent,
],
),
),

View File

@ -362,10 +362,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0"
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
source: hosted
version: "5.6.0"
version: "5.7.0"
dio_web_adapter:
dependency: transitive
description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.2.1+28
version: 1.2.1+34
environment:
sdk: ">=3.3.4 <4.0.0"
@ -68,9 +68,6 @@ dependencies:
firebase_crashlytics: ^4.0.4
firebase_analytics: ^11.2.1
firebase_performance: ^0.10.0+4
media_kit: ^1.1.10+1
media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4
flutter_svg: ^2.0.10+1
cross_file: ^0.3.4+2
google_fonts: ^6.2.1
@ -78,6 +75,9 @@ dependencies:
json_annotation: ^4.9.0
gap: ^3.0.1
fl_chart: ^0.69.0
media_kit: ^1.1.11
media_kit_video: ^1.2.5
media_kit_libs_video: ^1.0.5
dev_dependencies:
flutter_test: