2023-03-22 04:16:23 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
2023-11-29 18:16:22 +00:00
|
|
|
import 'package:provider/provider.dart';
|
2023-03-22 04:16:23 +00:00
|
|
|
|
2024-06-28 00:03:54 +00:00
|
|
|
import '../globals.dart';
|
2023-03-22 04:16:23 +00:00
|
|
|
import '../models/timeline_entry.dart';
|
2023-11-29 18:16:22 +00:00
|
|
|
import '../services/auth_service.dart';
|
2024-06-28 00:03:54 +00:00
|
|
|
import '../services/setting_service.dart';
|
2023-03-22 04:16:23 +00:00
|
|
|
import '../utils/clipboard_utils.dart';
|
|
|
|
import '../utils/url_opening_utils.dart';
|
2023-04-27 19:19:51 +00:00
|
|
|
import 'html_text_viewer_control.dart';
|
2023-03-22 04:16:23 +00:00
|
|
|
import 'media_attachment_viewer_control.dart';
|
|
|
|
import 'padding.dart';
|
|
|
|
import 'timeline/link_preview_control.dart';
|
|
|
|
import 'timeline/status_header_control.dart';
|
|
|
|
|
|
|
|
class SearchResultStatusControl extends StatefulWidget {
|
|
|
|
static final _logger = Logger('$SearchResultStatusControl');
|
|
|
|
final TimelineEntry status;
|
|
|
|
|
|
|
|
final Future Function() goToPostFunction;
|
|
|
|
|
|
|
|
const SearchResultStatusControl(this.status, this.goToPostFunction,
|
|
|
|
{super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<SearchResultStatusControl> createState() =>
|
|
|
|
_SearchResultStatusControlState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SearchResultStatusControlState extends State<SearchResultStatusControl> {
|
2024-06-28 00:03:54 +00:00
|
|
|
var showSpoilerControl = true;
|
2023-03-22 04:16:23 +00:00
|
|
|
var showContent = false;
|
|
|
|
|
|
|
|
TimelineEntry get status => widget.status;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
2023-10-31 01:44:16 +00:00
|
|
|
super.initState();
|
2024-06-28 00:03:54 +00:00
|
|
|
showSpoilerControl = getIt<SettingsService>().spoilerHidingEnabled;
|
|
|
|
showContent =
|
|
|
|
!showSpoilerControl ? true : widget.status.spoilerText.isEmpty;
|
2023-03-22 04:16:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
SearchResultStatusControl._logger
|
|
|
|
.finest('Building ${widget.status.toShortString()}');
|
|
|
|
const otherPadding = 8.0;
|
|
|
|
final body = Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Theme.of(context).dialogBackgroundColor,
|
|
|
|
border: Border.all(width: 0.5),
|
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
|
|
boxShadow: [
|
|
|
|
BoxShadow(
|
|
|
|
color: Theme.of(context).dividerColor,
|
|
|
|
blurRadius: 2,
|
2023-10-31 01:44:16 +00:00
|
|
|
offset: const Offset(4, 4),
|
2023-03-22 04:16:23 +00:00
|
|
|
spreadRadius: 0.1,
|
|
|
|
blurStyle: BlurStyle.normal,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.all(5.0),
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: StatusHeaderControl(
|
|
|
|
entry: widget.status,
|
|
|
|
showIsCommentText: true,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
buildMenuControl(context),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
2024-06-28 00:03:54 +00:00
|
|
|
if (showSpoilerControl && status.spoilerText.isNotEmpty)
|
2023-03-22 04:16:23 +00:00
|
|
|
TextButton(
|
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
showContent = !showContent;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
child: Text(
|
2024-06-28 00:03:54 +00:00
|
|
|
'Content Summary: ${status.spoilerText} (Click to ${showContent ? "Hide" : "Show"})')),
|
2023-03-22 04:16:23 +00:00
|
|
|
if (showContent) ...[
|
|
|
|
buildBody(context),
|
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
|
|
|
if (status.linkPreviewData != null)
|
|
|
|
LinkPreviewControl(preview: status.linkPreviewData!),
|
|
|
|
buildMediaBar(context),
|
|
|
|
],
|
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
2023-11-30 16:39:57 +00:00
|
|
|
return GestureDetector(
|
|
|
|
onTap: widget.goToPostFunction,
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.only(
|
|
|
|
left: otherPadding,
|
|
|
|
right: otherPadding,
|
|
|
|
top: otherPadding,
|
|
|
|
bottom: otherPadding,
|
|
|
|
),
|
|
|
|
child: body,
|
2023-03-22 04:16:23 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildBody(BuildContext context) {
|
2023-04-27 19:19:51 +00:00
|
|
|
return HtmlTextViewerControl(
|
|
|
|
content: widget.status.body,
|
|
|
|
onTapUrl: (url) async =>
|
|
|
|
await openUrlStringInSystembrowser(context, url, 'link'),
|
2023-03-22 04:16:23 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildMediaBar(BuildContext context) {
|
|
|
|
final items = widget.status.mediaAttachments;
|
|
|
|
if (items.isEmpty) {
|
|
|
|
return const SizedBox();
|
|
|
|
}
|
2023-11-29 17:38:21 +00:00
|
|
|
|
|
|
|
// A Link Preview with only one media attachment will have a duplicate image
|
|
|
|
// even though it points to different resources server side. So we don't
|
|
|
|
// want to render it twice.
|
|
|
|
if (widget.status.linkPreviewData != null && items.length == 1) {
|
|
|
|
return const SizedBox();
|
|
|
|
}
|
|
|
|
|
2023-11-29 18:16:22 +00:00
|
|
|
// A Diaspora reshare will have an HTML-built card with a link preview image
|
|
|
|
// to the same image as what would be in the single attachment but at a
|
|
|
|
// different link. So we don't want it to render twice.
|
|
|
|
final linkPhotoBaseUrl = Uri.https(
|
|
|
|
context.read<AccountsService>().currentProfile.serverName,
|
|
|
|
'photo/link',
|
|
|
|
).toString();
|
|
|
|
if (widget.status.body.contains(linkPhotoBaseUrl) && items.length == 1) {
|
|
|
|
return const SizedBox();
|
|
|
|
}
|
|
|
|
|
2023-03-22 04:16:23 +00:00
|
|
|
return SizedBox(
|
|
|
|
height: 250.0,
|
|
|
|
child: ListView.separated(
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
return MediaAttachmentViewerControl(
|
|
|
|
attachments: items,
|
|
|
|
index: index,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
separatorBuilder: (context, index) {
|
2023-10-31 01:44:16 +00:00
|
|
|
return const HorizontalPadding();
|
2023-03-22 04:16:23 +00:00
|
|
|
},
|
|
|
|
itemCount: items.length));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildMenuControl(BuildContext context) {
|
|
|
|
const goToPost = 'Open Post';
|
|
|
|
const copyText = 'Copy Post Text';
|
|
|
|
const copyUrl = 'Copy URL';
|
|
|
|
const openExternal = 'Open In Browser';
|
|
|
|
final options = [
|
|
|
|
goToPost,
|
|
|
|
copyText,
|
|
|
|
openExternal,
|
|
|
|
copyUrl,
|
|
|
|
];
|
|
|
|
|
|
|
|
return PopupMenuButton<String>(onSelected: (menuOption) async {
|
|
|
|
if (!context.mounted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (menuOption) {
|
|
|
|
case goToPost:
|
|
|
|
await widget.goToPostFunction();
|
|
|
|
break;
|
|
|
|
case openExternal:
|
|
|
|
await openUrlStringInSystembrowser(
|
|
|
|
context,
|
|
|
|
widget.status.externalLink,
|
|
|
|
'Status',
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case copyUrl:
|
|
|
|
await copyToClipboard(
|
|
|
|
context: context,
|
|
|
|
text: widget.status.externalLink,
|
|
|
|
message: 'Status link copied to clipboard',
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case copyText:
|
|
|
|
await copyToClipboard(
|
|
|
|
context: context,
|
|
|
|
text: widget.status.body,
|
|
|
|
message: 'Status text copied to clipboard',
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
//do nothing
|
|
|
|
}
|
|
|
|
}, itemBuilder: (context) {
|
|
|
|
return options
|
|
|
|
.map((o) => PopupMenuItem(value: o, child: Text(o)))
|
|
|
|
.toList();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|