Link expand

This commit is contained in:
2024-08-19 19:36:01 +08:00
parent 95ea3e558f
commit cf1cfecb08
16 changed files with 461 additions and 103 deletions

View File

@ -252,7 +252,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
.listMetadata(widget.initialAttachments ?? List.empty())
.then((result) {
setState(() {
_attachments = result;
_attachments = List.from(result, growable: true);
_isBusy = false;
_isFirstTimeBusy = false;
});

View File

@ -1,14 +1,14 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.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/services.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:video_player/video_player.dart';
class AttachmentItem extends StatefulWidget {
final String parentId;
@ -231,22 +231,20 @@ class _AttachmentItemVideo extends StatefulWidget {
}
class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
VideoPlayerController? _playerController;
ChewieController? _chewieController;
late final _player = Player(
configuration: const PlayerConfiguration(
logLevel: MPVLogLevel.error,
),
);
late final _controller = VideoController(_player);
bool _showContent = false;
Future<void> _startLoad() async {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
_playerController = VideoPlayerController.networkUrl(
Uri.parse(
ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}'),
),
);
_playerController!.initialize();
_chewieController = ChewieController(
aspectRatio: ratio,
videoPlayerController: _playerController!,
await _player.open(
Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}')),
play: false,
);
setState(() => _showContent = true);
}
@ -259,17 +257,10 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
}
}
@override
void dispose() {
_playerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
if (!_showContent || _chewieController == null) {
if (!_showContent) {
return GestureDetector(
child: AspectRatio(
aspectRatio: ratio,
@ -307,8 +298,15 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
);
}
return Chewie(
controller: _chewieController!,
return Video(
aspectRatio: ratio,
controller: _controller,
);
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
}

View File

@ -73,7 +73,7 @@ class _AttachmentListState extends State<AttachmentList> {
int portrait = 0, square = 0, landscape = 0;
for (var entry in _attachmentsMeta) {
if (entry == null) continue;
if (entry!.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] is int) {
consistentValue ??= entry.metadata?['ratio'].toDouble();
} else {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/event.dart';
import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart';
class ChatEventMessage extends StatelessWidget {
@ -53,11 +54,28 @@ class ChatEventMessage extends StatelessWidget {
);
}
Widget _buildLinkExpansion() {
final body = EventMessageBody.fromJson(item.body);
return LinkExpansion(content: body.text);
}
Widget _buildBody(BuildContext context) {
if (isMerged) {
return _buildContent(context).paddingOnly(left: 52);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildContent(context).paddingOnly(left: 4),
_buildLinkExpansion(),
],
).paddingOnly(left: 48);
} else {
return _buildContent(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildContent(context),
_buildLinkExpansion(),
],
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:get/get.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LinkExpansion extends StatelessWidget {
final String content;
const LinkExpansion({super.key, required this.content});
@override
Widget build(BuildContext context) {
final linkRegex = RegExp(
r'(?:(?:https?|ftp):\/\/|www\.)'
r'(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)'
r'[^\s<]*'
r'[^\s<?!.,:*_~]',
);
final matches = linkRegex.allMatches(content);
if (matches.isEmpty) {
return const SizedBox();
}
final LinkExpandController expandController = Get.find();
return Column(
children: matches.map((x) {
return FutureBuilder(
future: expandController.expandLink(x.group(0)!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final isRichDescription = [
"solsynth.dev",
].contains(Uri.parse(snapshot.data!.url).host);
return GestureDetector(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ([
snapshot.data!.icon != null &&
(snapshot.data!.icon?.startsWith('http') ?? false),
snapshot.data!.siteName != null
].any((x) => x))
Row(
children: [
if (snapshot.data!.icon != null &&
(snapshot.data!.icon?.startsWith('http') ??
false))
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: Image.network(
snapshot.data!.icon!,
width: 32,
height: 32,
),
).paddingOnly(right: 8),
if (snapshot.data!.siteName != null)
Text(
snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
),
],
).paddingOnly(bottom: 8),
if (snapshot.data!.image != null &&
(snapshot.data!.image?.startsWith('http') ?? false))
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: Image.network(
snapshot.data!.image!,
),
).paddingOnly(bottom: 8),
Text(
snapshot.data!.title ?? 'No Title',
maxLines: 1,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyLarge,
),
if (snapshot.data!.description != null && isRichDescription)
MarkdownBody(data: snapshot.data!.description!)
else if (snapshot.data!.description != null)
Text(
snapshot.data!.description!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
).paddingAll(12),
),
onTap: () {
launchUrlString(x.group(0)!);
},
);
},
);
}).toList(),
);
}
}