Compare commits

...

3 Commits

Author SHA1 Message Date
d7c1ffe3cc 💄 Optimization of sharing post via image 2024-12-13 00:05:18 +08:00
240ad7dc7e Share the post via image 2024-12-12 23:51:27 +08:00
bb5fe9c380 💄 Optimize font color on app bar in some pages 2024-12-12 22:33:10 +08:00
13 changed files with 364 additions and 84 deletions

View File

@ -403,6 +403,7 @@
"accountStatusOffline": "Offline", "accountStatusOffline": "Offline",
"accountStatusLastSeen": "Last seen at {}", "accountStatusLastSeen": "Last seen at {}",
"postArticle": "Article on the Solar Network", "postArticle": "Article on the Solar Network",
"postStory": "Story on the Solar Network",
"articleWrittenAt": "Written at {}", "articleWrittenAt": "Written at {}",
"articleEditedAt": "Edited at {}", "articleEditedAt": "Edited at {}",
"attachmentSaved": "Saved to album", "attachmentSaved": "Saved to album",
@ -436,5 +437,9 @@
"publisherBlockHint": "Block {}", "publisherBlockHint": "Block {}",
"publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.", "publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.",
"userUnblocked": "{} has been unblocked.", "userUnblocked": "{} has been unblocked.",
"userBlocked": "{} has been blocked." "userBlocked": "{} has been blocked.",
"postSharingViaPicture": "Capturing post as picture, please stand by...",
"postImageShareAds": "Explore posts on the Solar Network",
"postShare": "Share",
"postShareImage": "Share via Image"
} }

View File

@ -401,6 +401,7 @@
"accountStatusOffline": "离线", "accountStatusOffline": "离线",
"accountStatusLastSeen": "最后一次在 {} 上线", "accountStatusLastSeen": "最后一次在 {} 上线",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事",
"articleWrittenAt": "发表于 {}", "articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}", "articleEditedAt": "编辑于 {}",
"attachmentSaved": "已保存到相册", "attachmentSaved": "已保存到相册",
@ -434,5 +435,9 @@
"publisherBlockHint": "屏蔽 {}", "publisherBlockHint": "屏蔽 {}",
"publisherBlockHintDescription": "你正要屏蔽此发布者的运营者,该操作也将屏蔽由同一用户运营的发布者。", "publisherBlockHintDescription": "你正要屏蔽此发布者的运营者,该操作也将屏蔽由同一用户运营的发布者。",
"userUnblocked": "已解除屏蔽用户 {}", "userUnblocked": "已解除屏蔽用户 {}",
"userBlocked": "已屏蔽用户 {}" "userBlocked": "已屏蔽用户 {}",
"postSharingViaPicture": "正在生成帖子截图,请稍等片刻……",
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
"postShare": "分享",
"postShareImage": "分享帖图"
} }

View File

@ -4,8 +4,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- croppy (0.0.1): - croppy (0.0.1):
- Flutter - Flutter
- cupertino_http (0.0.1):
- Flutter
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- DKImagePickerController/Core (4.3.9): - DKImagePickerController/Core (4.3.9):
@ -196,11 +194,6 @@ PODS:
- SDWebImage (5.20.0): - SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0) - SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0) - SDWebImage/Core (5.20.0)
- Sentry/HybridSDK (8.41.0)
- sentry_flutter (8.11.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.41.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -221,7 +214,6 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
@ -242,7 +234,6 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
@ -267,7 +258,6 @@ SPEC REPOS:
- PromisesObjC - PromisesObjC
- SAMKeychain - SAMKeychain
- SDWebImage - SDWebImage
- Sentry
- SwiftyGif - SwiftyGif
- WebRTC-SDK - WebRTC-SDK
@ -276,8 +266,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/connectivity_plus/darwin" :path: ".symlinks/plugins/connectivity_plus/darwin"
croppy: croppy:
:path: ".symlinks/plugins/croppy/ios" :path: ".symlinks/plugins/croppy/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
@ -318,8 +306,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
screen_brightness_ios: screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios" :path: ".symlinks/plugins/screen_brightness_ios/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
@ -336,7 +322,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321 croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
@ -372,8 +357,6 @@ SPEC CHECKSUMS:
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
Sentry: 54d0fe6c0df448497c8ed4cce66ccf7027e1823e
sentry_flutter: 83a84efb7f978522c9127fbc0c07dab09663eecc
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d

View File

@ -53,6 +53,11 @@ class SnPostContentProvider {
if (out.body['thumbnail'] != null) { if (out.body['thumbnail'] != null) {
rids.add(out.body['thumbnail']); rids.add(out.body['thumbnail']);
} }
if (out.repostId != null) {
out = out.copyWith(
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
);
}
final attachments = await _attach.getMultiple(rids.toList()); final attachments = await _attach.getMultiple(rids.toList());
out = out.copyWith( out = out.copyWith(

View File

@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -238,7 +238,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
TextSpan( TextSpan(
text: _account!.nick, text: _account!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white, color: Theme.of(context).appBarTheme.foregroundColor!,
shadows: labelShadows, shadows: labelShadows,
), ),
), ),
@ -246,7 +246,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
TextSpan( TextSpan(
text: '@${_account!.name}', text: '@${_account!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Colors.white, color: Theme.of(context).appBarTheme.foregroundColor!,
shadows: labelShadows, shadows: labelShadows,
), ),
), ),

View File

@ -84,12 +84,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(), text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white), style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: 'postDetail'.tr(), text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white), style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
), ),
]), ]),
) )

View File

@ -149,12 +149,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(), text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white), style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: PostWriteController.kTitleMap[widget.mode]!.tr(), text: PostWriteController.kTitleMap[widget.mode]!.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white), style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
), ),
]), ]),
), ),

View File

@ -201,7 +201,6 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
setState(() => _isWorking = true); setState(() => _isWorking = true);
try { try {
final sn = context.read<SnNetworkProvider>();
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {}); await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return; if (!mounted) return;
@ -288,7 +287,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
TextSpan( TextSpan(
text: _publisher!.nick, text: _publisher!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white, color: Theme.of(context).appBarTheme.foregroundColor!,
shadows: labelShadows, shadows: labelShadows,
), ),
), ),

View File

@ -1,5 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -14,19 +15,21 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data; final List<SnAttachment?> data;
final bool bordered; final bool bordered;
final bool noGrow; final bool noGrow;
final bool isFlatted;
final double? maxHeight; final double? maxHeight;
final EdgeInsets? listPadding; final EdgeInsets? listPadding;
const AttachmentList({ const AttachmentList({
super.key, super.key,
required this.data, required this.data,
this.bordered = false, this.bordered = false,
this.noGrow = false, this.noGrow = false,
this.isFlatted = false,
this.maxHeight, this.maxHeight,
this.listPadding, this.listPadding,
}); });
static const BorderRadius kDefaultRadius = static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
BorderRadius.all(Radius.circular(8));
@override @override
State<AttachmentList> createState() => _AttachmentListState(); State<AttachmentList> createState() => _AttachmentListState();
@ -44,9 +47,8 @@ class _AttachmentListState extends State<AttachmentList> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, layoutConstraints) { builder: (context, layoutConstraints) {
final borderSide = widget.bordered final borderSide =
? BorderSide(width: 1, color: Theme.of(context).dividerColor) widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
: BorderSide.none;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer; final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final constraints = BoxConstraints( final constraints = BoxConstraints(
minWidth: 80, minWidth: 80,
@ -56,14 +58,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]?.metadata['ratio']?.toDouble() ??
widget.data[0]?.metadata['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(
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE) constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
@ -79,8 +80,7 @@ class _AttachmentListState extends State<AttachmentList> {
child: GestureDetector( child: GestureDetector(
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
widget.noGrow) {
return Padding( return Padding(
// Single child list-like displaying // Single child list-like displaying
padding: widget.listPadding ?? EdgeInsets.zero, padding: widget.listPadding ?? EdgeInsets.zero,
@ -129,6 +129,37 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
if (widget.isFlatted) {
return Wrap(
spacing: 4,
runSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => AspectRatio(
aspectRatio: (ele?.metadata['ratio'] ?? 1).toDouble(),
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
),
),
),
),
)
.toList(),
);
}
return AspectRatio( return AspectRatio(
aspectRatio: (widget.data.firstOrNull?.metadata['ratio'] ?? 1).toDouble(), aspectRatio: (widget.data.firstOrNull?.metadata['ratio'] ?? 1).toDouble(),
child: Container( child: Container(
@ -147,9 +178,7 @@ class _AttachmentListState extends State<AttachmentList> {
onTap: () { onTap: () {
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: widget.data data: widget.data.where((ele) => ele != null).cast(),
.where((ele) => ele != null)
.cast(),
initialIndex: idx, initialIndex: idx,
heroTags: heroTags, heroTags: heroTags,
), ),

View File

@ -5,10 +5,15 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart';
import 'package:popover/popover.dart'; import 'package:popover/popover.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -52,10 +57,66 @@ class PostItem extends StatelessWidget {
if (onChanged != null) onChanged!(data); if (onChanged != null) onChanged!(data);
} }
void _doShare(BuildContext context) {
final box = context.findRenderObject() as RenderBox?;
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} else {
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}
}
void _doShareViaPicture(BuildContext context) async {
final box = context.findRenderObject() as RenderBox?;
context.showSnackbar('postSharingViaPicture'.tr());
final controller = ScreenshotController();
final capturedImage = await controller.captureFromLongWidget(
InheritedTheme.captureAll(
context,
MediaQuery(
data: MediaQuery.of(context),
child: Material(
child: MultiProvider(
providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()),
],
child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
child: PostShareImage(data: data),
),
),
),
),
),
pixelRatio: 3,
context: context,
);
if (kIsWeb) return;
final directory = await getTemporaryDirectory();
final imagePath = await File(
'${directory.path}/sn-share-via-image-${DateTime.now().millisecondsSinceEpoch}.png',
).create();
await imagePath.writeAsBytes(capturedImage);
await Share.shareXFiles(
[XFile(imagePath.path)],
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
await imagePath.delete();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user!.id;
// Article headline preview // Article headline preview
if (!showFullPost && data.type == 'article') { if (!showFullPost && data.type == 'article') {
return Container( return Container(
@ -65,6 +126,9 @@ class PostItem extends StatelessWidget {
children: [ children: [
_PostContentHeader( _PostContentHeader(
data: data, data: data,
isAuthor: isAuthor,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onDeleted: () { onDeleted: () {
if (onDeleted != null) {} if (onDeleted != null) {}
}, },
@ -116,6 +180,8 @@ class PostItem extends StatelessWidget {
data: data, data: data,
showComments: showComments, showComments: showComments,
showReactions: showReactions, showReactions: showReactions,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onChanged: _onChanged, onChanged: _onChanged,
).padding(left: 8, right: 14), ).padding(left: 8, right: 14),
], ],
@ -134,6 +200,8 @@ class PostItem extends StatelessWidget {
_PostContentHeader( _PostContentHeader(
data: data, data: data,
showMenu: showMenu, showMenu: showMenu,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onDeleted: () { onDeleted: () {
if (onDeleted != null) onDeleted!(); if (onDeleted != null) onDeleted!();
}, },
@ -181,6 +249,8 @@ class PostItem extends StatelessWidget {
data: data, data: data,
showComments: showComments, showComments: showComments,
showReactions: showReactions, showReactions: showReactions,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onChanged: _onChanged, onChanged: _onChanged,
).padding(left: 8, right: 14), ).padding(left: 8, right: 14),
], ],
@ -191,28 +261,145 @@ class PostItem extends StatelessWidget {
} }
} }
class PostShareImage extends StatelessWidget {
const PostShareImage({
super.key,
required this.data,
});
final SnPost data;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 480,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_PostContentHeader(
data: data,
onDeleted: () {},
onShare: () {},
onShareImage: () {},
showMenu: false,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
_PostHeadline(
data: data,
isEnlarge: data.type == 'article',
).padding(horizontal: 16, bottom: 8),
_PostContentBody(
data: data,
isEnlarge: data.type == 'article',
).padding(horizontal: 16, bottom: 8),
if (data.repostTo != null)
_PostQuoteContent(
child: data.repostTo!,
isRelativeDate: false,
isFlatted: true,
).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
AttachmentList(
data: data.preload!.attachments!,
isFlatted: true,
).padding(horizontal: 16, bottom: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.visibility > 0) _PostVisibilityHint(data: data),
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
],
).padding(horizontal: 16),
_PostBottomAction(
data: data,
showComments: true,
showReactions: true,
onShare: () {},
onShareImage: () {},
onChanged: (SnPost data) {},
).padding(left: 8, right: 14),
const Divider(height: 1),
const Gap(12),
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${data.aliasPrefix} / ${data.alias ?? '#${data.id}'}',
style: GoogleFonts.robotoMono(fontSize: 17),
),
const Gap(2),
Text(
switch (data.type) {
'article' => 'postArticle'.tr(),
_ => 'postStory'.tr(),
},
style: GoogleFonts.robotoMono(fontSize: 12),
),
],
),
),
Text(
'postImageShareAds',
style: GoogleFonts.robotoMono(fontSize: 13),
).tr(),
],
),
),
QrImageView(
padding: EdgeInsets.zero,
data: 'https://solsynth.dev/posts/${data.id}',
version: QrVersions.auto,
size: 100,
gapless: true,
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(32, 32),
),
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.circle,
color: Theme.of(context).colorScheme.onSurface,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
)
],
),
).padding(left: 16, right: 32, vertical: 8),
],
).padding(vertical: 16),
);
}
}
class _PostBottomAction extends StatelessWidget { class _PostBottomAction extends StatelessWidget {
final SnPost data; final SnPost data;
final bool showComments; final bool showComments;
final bool showReactions; final bool showReactions;
final Function(SnPost data) onChanged; final Function(SnPost data) onChanged;
final Function() onShare, onShareImage;
const _PostBottomAction({ const _PostBottomAction({
required this.data, required this.data,
required this.showComments, required this.showComments,
required this.showReactions, required this.showReactions,
required this.onChanged, required this.onChanged,
required this.onShare,
required this.onShareImage,
}); });
void _doShare() {
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url));
} else {
Share.share(url);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha( final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha(
@ -297,7 +484,8 @@ class _PostBottomAction extends StatelessWidget {
..removeLast(), ..removeLast(),
), ),
InkWell( InkWell(
onTap: _doShare, onTap: onShare,
onLongPress: onShareImage,
child: Icon( child: Icon(
Symbols.share, Symbols.share,
size: 20, size: 20,
@ -369,10 +557,10 @@ class _PostHeadline extends StatelessWidget {
style: TextStyle(fontSize: 13), style: TextStyle(fontSize: 13),
), ),
const Gap(8), const Gap(8),
if (data.updatedAt != data.createdAt) if (data.editedAt != null)
Text( Text(
'articleEditedAt'.tr( 'articleEditedAt'.tr(
args: [DateFormat('y/M/d HH:mm').format(data.updatedAt)], args: [DateFormat('y/M/d HH:mm').format(data.editedAt!)],
), ),
style: TextStyle(fontSize: 13), style: TextStyle(fontSize: 13),
), ),
@ -405,15 +593,22 @@ class _PostHeadline extends StatelessWidget {
class _PostContentHeader extends StatelessWidget { class _PostContentHeader extends StatelessWidget {
final SnPost data; final SnPost data;
final bool isAuthor;
final bool isCompact; final bool isCompact;
final bool isRelativeDate;
final bool showMenu; final bool showMenu;
final Function onDeleted; final Function onDeleted;
final Function() onShare, onShareImage;
const _PostContentHeader({ const _PostContentHeader({
required this.data, required this.data,
this.isAuthor = false,
this.isCompact = false, this.isCompact = false,
this.isRelativeDate = true,
this.showMenu = true, this.showMenu = true,
required this.onDeleted, required this.onDeleted,
required this.onShare,
required this.onShareImage,
}); });
Future<void> _deletePost(BuildContext context) async { Future<void> _deletePost(BuildContext context) async {
@ -441,9 +636,6 @@ class _PostContentHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user!.id;
return Row( return Row(
children: [ children: [
GestureDetector( GestureDetector(
@ -479,9 +671,11 @@ class _PostContentHeader extends StatelessWidget {
children: [ children: [
Text('@${data.publisher.name}').fontSize(13), Text('@${data.publisher.name}').fontSize(13),
const Gap(4), const Gap(4),
Text(RelativeTime(context).format( Text(
data.publishedAt ?? data.createdAt, isRelativeDate
)).fontSize(13), ? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
).fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
], ],
@ -496,9 +690,11 @@ class _PostContentHeader extends StatelessWidget {
children: [ children: [
Text('@${data.publisher.name}').fontSize(13), Text('@${data.publisher.name}').fontSize(13),
const Gap(4), const Gap(4),
Text(RelativeTime(context).format( Text(
data.publishedAt ?? data.createdAt, isRelativeDate
)).fontSize(13), ? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
).fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
], ],
@ -573,6 +769,27 @@ class _PostContentHeader extends StatelessWidget {
}, },
), ),
const PopupMenuDivider(), const PopupMenuDivider(),
PopupMenuItem(
onTap: onShare,
child: Row(
children: [
const Icon(Symbols.share),
const Gap(16),
Text('postShare').tr(),
],
),
),
PopupMenuItem(
onTap: onShareImage,
child: Row(
children: [
const Icon(Symbols.share_reviews),
const Gap(16),
Text('postShareImage').tr(),
],
),
),
const PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@ -623,8 +840,15 @@ class _PostContentBody extends StatelessWidget {
class _PostQuoteContent extends StatelessWidget { class _PostQuoteContent extends StatelessWidget {
final SnPost child; final SnPost child;
final bool isRelativeDate;
final bool isFlatted;
const _PostQuoteContent({super.key, required this.child}); const _PostQuoteContent({
super.key,
this.isRelativeDate = true,
this.isFlatted = false,
required this.child,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -645,14 +869,17 @@ class _PostQuoteContent extends StatelessWidget {
_PostContentHeader( _PostContentHeader(
data: child, data: child,
isCompact: true, isCompact: true,
isRelativeDate: isRelativeDate,
showMenu: false, showMenu: false,
onShare: () {},
onShareImage: () {},
onDeleted: () {}, onDeleted: () {},
).padding(bottom: 4), ).padding(bottom: 4),
_PostContentBody(data: child), _PostContentBody(data: child),
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4), if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
if (child.preload?.attachments?.isNotEmpty ?? false) if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8), bottomLeft: Radius.circular(8),
@ -660,12 +887,15 @@ class _PostQuoteContent extends StatelessWidget {
), ),
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.preload!.attachments!,
isFlatted: isFlatted,
listPadding: const EdgeInsets.symmetric(horizontal: 12), listPadding: const EdgeInsets.symmetric(horizontal: 12),
), ),
).padding( ).padding(
top: 8, top: 8,
bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0, bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0,
), )
else
const Gap(8),
], ],
), ),
), ),

View File

@ -266,10 +266,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: connectivity_plus name: connectivity_plus
sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3" sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.1"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -362,18 +362,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: device_info_plus name: device_info_plus
sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95 sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.1.1" version: "11.2.0"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: device_info_plus_platform_interface name: device_info_plus_platform_interface
sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.2"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1190,18 +1190,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.1" version: "8.1.2"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
pasteboard: pasteboard:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1402,6 +1402,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
relative_time: relative_time:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1502,18 +1518,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8" sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.2" version: "10.1.3"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: share_plus_platform_interface name: share_plus_platform_interface
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48 sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.1" version: "5.0.2"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -99,6 +99,7 @@ dependencies:
package_info_plus: ^8.1.1 package_info_plus: ^8.1.1
intl: ^0.19.0 intl: ^0.19.0
screenshot: ^3.0.0 screenshot: ^3.0.0
qr_flutter: ^4.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: