Compare commits

..

4 Commits

Author SHA1 Message Date
997934f680 🚀 Launch 2.3.2+71 2025-02-23 14:51:15 +08:00
26e69d6264 Desktop local notification 2025-02-23 14:49:38 +08:00
153eabcbf2 💄 Enlarge emote when there is only one 2025-02-23 14:40:40 +08:00
6d0145c335 💄 Make attachment in chat aligned with message 2025-02-23 14:35:51 +08:00
15 changed files with 224 additions and 86 deletions

View File

@@ -55,6 +55,7 @@ jobs:
sudo apt-get install libmpv-dev mpv sudo apt-get install libmpv-dev mpv
sudo apt-get install libayatana-appindicator3-dev sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install keybinder-3.0 sudo apt-get install keybinder-3.0
sudo apt-get install libnotify-dev
- run: flutter pub get - run: flutter pub get
- run: flutter build linux - run: flutter build linux
- name: Archive production artifacts - name: Archive production artifacts

View File

@@ -717,5 +717,6 @@
"fieldStickerAttachment": "Attachment", "fieldStickerAttachment": "Attachment",
"stickersNew": "New Sticker", "stickersNew": "New Sticker",
"stickersNewDescription": "Create a new sticker belongs to this pack.", "stickersNewDescription": "Create a new sticker belongs to this pack.",
"stickersPackNew": "New Sticker Pack" "stickersPackNew": "New Sticker Pack",
"trayMenuShow": "Show"
} }

View File

@@ -715,5 +715,6 @@
"fieldStickerAttachment": "附件", "fieldStickerAttachment": "附件",
"stickersNew": "新建贴图", "stickersNew": "新建贴图",
"stickersNewDescription": "创建一个新的贴图。", "stickersNewDescription": "创建一个新的贴图。",
"stickersPackNew": "新建贴图包" "stickersPackNew": "新建贴图包",
"trayMenuShow": "显示"
} }

View File

@@ -48,6 +48,7 @@ import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:local_notifier/local_notifier.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
void appBackgroundDispatcher() { void appBackgroundDispatcher() {
@@ -350,6 +351,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
disabled: true, disabled: true,
), ),
MenuItem.separator(), MenuItem.separator(),
MenuItem(
key: 'window_show',
label: 'trayMenuShow'.tr(),
),
MenuItem( MenuItem(
key: 'exit', key: 'exit',
label: 'trayMenuExit'.tr(), label: 'trayMenuExit'.tr(),
@@ -359,6 +364,15 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
await trayManager.setContextMenu(menu); await trayManager.setContextMenu(menu);
} }
Future<void> _notifyInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup(
appName: 'solian',
shortcutPolicy: ShortcutPolicy.requireCreate,
);
}
AppLifecycleListener? _appLifecycleListener; AppLifecycleListener? _appLifecycleListener;
@override @override
@@ -373,6 +387,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_trayInitialization(); _trayInitialization();
_hotkeyInitialization(); _hotkeyInitialization();
_notifyInitialization();
_initialize().then((_) { _initialize().then((_) {
_postInitialization(); _postInitialization();
_tryRequestRating(); _tryRequestRating();
@@ -408,6 +423,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
@override @override
void onTrayMenuItemClick(MenuItem menuItem) { void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) { switch (menuItem.key) {
case 'window_show':
appWindow.show();
break;
case 'exit': case 'exit':
_appLifecycleListener?.dispose(); _appLifecycleListener?.dispose();
SystemChannels.platform.invokeMethod('SystemNavigator.pop'); SystemChannels.platform.invokeMethod('SystemNavigator.pop');

View File

@@ -1,11 +1,13 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@@ -92,6 +94,20 @@ class NotificationProvider extends ChangeNotifier {
updateTray(); updateTray();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact(); if (doHaptic) HapticFeedback.mediumImpact();
if (!kIsWeb) {
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
LocalNotification notify = LocalNotification(
title: notification.title,
subtitle: notification.subtitle,
body: notification.body,
);
notify.onClick = () {
appWindow.show();
};
notify.show();
}
}
} }
}); });
} }

View File

@@ -109,11 +109,13 @@ class ChatMessage extends StatelessWidget {
onTap: () { onTap: () {
if (user == null) return; if (user == null) return;
showPopover( showPopover(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor:
Theme.of(context).colorScheme.surface,
context: context, context: context,
transition: PopoverTransition.other, transition: PopoverTransition.other,
bodyBuilder: (context) => SizedBox( bodyBuilder: (context) => SizedBox(
width: math.min(400, MediaQuery.of(context).size.width - 10), width: math.min(
400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard( child: AccountPopoverCard(
data: user, data: user,
), ),
@@ -144,11 +146,14 @@ class ChatMessage extends StatelessWidget {
radius: 12, radius: 12,
).padding(right: 8), ).padding(right: 8),
Text( Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown', (data.sender.nick?.isNotEmpty ?? false)
? data.sender.nick!
: user?.nick ?? 'unknown',
).bold(), ).bold(),
const Gap(8), const Gap(8),
Text( Text(
dateFormatter.format(data.createdAt.toLocal()), dateFormatter
.format(data.createdAt.toLocal()),
).fontSize(13), ).fontSize(13),
], ],
).height(21), ).height(21),
@@ -159,7 +164,8 @@ class ChatMessage extends StatelessWidget {
maxWidth: 480, maxWidth: 480,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius:
const BorderRadius.all(Radius.circular(8)),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@@ -207,9 +213,12 @@ class ChatMessage extends StatelessWidget {
maxHeight: 560, maxHeight: 560,
maxWidth: 480, maxWidth: 480,
minWidth: 480, minWidth: 480,
padding: padding.copyWith(top: 8), padding: padding.copyWith(top: 8, left: 48 + padding.left),
), ),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8), if (!hasMerged && !isCompact)
const Gap(12)
else if (!isCompact)
const Gap(8),
], ],
), ),
), ),
@@ -223,7 +232,8 @@ class _ChatMessageText extends StatelessWidget {
final Function(SnChatMessage)? onEdit; final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete; final Function(SnChatMessage)? onDelete;
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete}); const _ChatMessageText(
{required this.data, this.onReply, this.onEdit, this.onDelete});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -237,7 +247,8 @@ class _ChatMessageText extends StatelessWidget {
children: [ children: [
SelectionArea( SelectionArea(
contextMenuBuilder: (context, editableTextState) { contextMenuBuilder: (context, editableTextState) {
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems; final List<ContextMenuButtonItem> items =
editableTextState.contextMenuButtonItems;
if (onReply != null) { if (onReply != null) {
items.insert( items.insert(
@@ -286,6 +297,8 @@ class _ChatMessageText extends StatelessWidget {
child: MarkdownTextContent( child: MarkdownTextContent(
content: data.body['text'], content: data.body['text'],
isAutoWarp: true, isAutoWarp: true,
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
), ),
), ),
), ),

View File

@@ -44,7 +44,9 @@ class MarkdownTextContent extends StatelessWidget {
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaler: textScaler, textScaler: textScaler,
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null, p: textColor != null
? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor)
: null,
blockquote: TextStyle( blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
@@ -115,7 +117,7 @@ class MarkdownTextContent extends StatelessWidget {
final alias = segments[1]; final alias = segments[1];
final st = context.read<SnStickerProvider>(); final st = context.read<SnStickerProvider>();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final double size = isEnlargeSticker ? 128 : 32; final double size = isEnlargeSticker ? 96 : 32;
return Container( return Container(
width: size, width: size,
height: size, height: size,
@@ -131,7 +133,8 @@ class MarkdownTextContent extends StatelessWidget {
if (snapshot.hasData) { if (snapshot.hasData) {
return GestureDetector( return GestureDetector(
child: UniversalImage( child: UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid), sn.getAttachmentUrl(
snapshot.data!.attachment.rid),
fit: BoxFit.contain, fit: BoxFit.contain,
width: size, width: size,
height: size, height: size,
@@ -177,7 +180,9 @@ class MarkdownTextContent extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: AspectRatio(
aspectRatio: attachment.metadata['ratio'] ?? aspectRatio: attachment.metadata['ratio'] ??
switch (attachment.mimetype.split('/').firstOrNull) { switch (attachment.mimetype
.split('/')
.firstOrNull) {
'audio' => 16 / 9, 'audio' => 16 / 9,
'video' => 16 / 9, 'video' => 16 / 9,
_ => 1, _ => 1,

View File

@@ -92,9 +92,10 @@ class OpenablePostItem extends StatelessWidget {
openColor: Colors.transparent, openColor: Colors.transparent,
openElevation: 0, openElevation: 0,
transitionType: ContainerTransitionType.fade, transitionType: ContainerTransitionType.fade,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( closedColor:
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
), cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder( closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
@@ -135,9 +136,11 @@ class PostItem extends StatelessWidget {
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
final url = 'https://solsynth.dev/posts/${data.id}'; final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); Share.shareUri(Uri.parse(url),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} else { } else {
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); Share.share(url,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} }
} }
@@ -155,7 +158,8 @@ class PostItem extends StatelessWidget {
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()), Provider<SnNetworkProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()), ChangeNotifierProvider<ConfigProvider>(
create: (_) => context.read()),
], ],
child: ResponsiveBreakpoints.builder( child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints, breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
@@ -183,7 +187,8 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} else { } else {
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile); await FileSaver.instance.saveFile(
name: 'Solar Network Post #${data.id}.png', file: imageFile);
} }
await imageFile.delete(); await imageFile.delete();
@@ -197,7 +202,9 @@ class PostItem extends StatelessWidget {
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id; final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
// Video full view // Video full view
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) { if (showFullPost &&
data.type == 'video' &&
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -217,7 +224,8 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {} if (onDeleted != null) {}
}, },
).padding(bottom: 8), ).padding(bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8), if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(bottom: 8),
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8), _PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
_PostFeaturedComment(data: data), _PostFeaturedComment(data: data),
_PostBottomAction( _PostBottomAction(
@@ -265,7 +273,8 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {} if (onDeleted != null) {}
}, },
).padding(horizontal: 12, top: 8, bottom: 8), ).padding(horizontal: 12, top: 8, bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
Container( Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12), margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
@@ -308,8 +317,13 @@ class PostItem extends StatelessWidget {
], ],
), ),
), ),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), Text('postArticle')
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), .tr()
.fontSize(13)
.opacity(0.75)
.padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
_PostBottomAction( _PostBottomAction(
data: data, data: data,
showComments: showComments, showComments: showComments,
@@ -324,7 +338,8 @@ class PostItem extends StatelessWidget {
} }
final displayableAttachments = data.preload?.attachments final displayableAttachments = data.preload?.attachments
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') ?.where((ele) =>
ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList(); .toList();
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
@@ -349,9 +364,13 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) onDeleted!(); if (onDeleted != null) onDeleted!();
}, },
).padding(horizontal: 12, vertical: 8), ).padding(horizontal: 12, vertical: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), if (data.preload?.video != null)
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.body['title'] != null || data.body['description'] != null) if (data.type == 'question')
_PostQuestionHint(data: data)
.padding(horizontal: 16, bottom: 8),
if (data.body['title'] != null ||
data.body['description'] != null)
_PostHeadline( _PostHeadline(
data: data, data: data,
isEnlarge: data.type == 'article' && showFullPost, isEnlarge: data.type == 'article' && showFullPost,
@@ -365,7 +384,8 @@ class PostItem extends StatelessWidget {
if (data.repostTo != null) if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding( _PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12, horizontal: 12,
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0, bottom:
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
), ),
if (data.visibility > 0) if (data.visibility > 0)
_PostVisibilityHint(data: data).padding( _PostVisibilityHint(data: data).padding(
@@ -377,7 +397,9 @@ class PostItem extends StatelessWidget {
horizontal: 16, horizontal: 16,
vertical: 4, vertical: 4,
), ),
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6), if (data.tags.isNotEmpty)
_PostTagsList(data: data)
.padding(horizontal: 16, top: 4, bottom: 6),
], ],
), ),
), ),
@@ -390,12 +412,16 @@ class PostItem extends StatelessWidget {
fit: showFullPost ? BoxFit.cover : BoxFit.contain, fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4), if (data.preload?.poll != null)
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) PostPoll(poll: data.preload!.poll!)
.padding(horizontal: 12, vertical: 4),
if (data.body['content'] != null &&
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
LinkPreviewWidget( LinkPreviewWidget(
text: data.body['content'], text: data.body['content'],
).padding(horizontal: 4), ).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), _PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
Container( Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column( child: Column(
@@ -457,7 +483,8 @@ class PostShareImageWidget extends StatelessWidget {
showMenu: false, showMenu: false,
isRelativeDate: false, isRelativeDate: false,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), if (data.type == 'question')
_PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
_PostHeadline( _PostHeadline(
data: data, data: data,
isEnlarge: data.type == 'article', isEnlarge: data.type == 'article',
@@ -472,7 +499,8 @@ class PostShareImageWidget extends StatelessWidget {
child: data.repostTo!, child: data.repostTo!,
isRelativeDate: false, isRelativeDate: false,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) if (data.type != 'article' &&
(data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
columned: true, columned: true,
@@ -481,7 +509,8 @@ class PostShareImageWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (data.visibility > 0) _PostVisibilityHint(data: data), if (data.visibility > 0) _PostVisibilityHint(data: data),
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data), if (data.body['content_truncated'] == true)
_PostTruncatedHint(data: data),
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
_PostBottomAction( _PostBottomAction(
@@ -541,7 +570,8 @@ class PostShareImageWidget extends StatelessWidget {
version: QrVersions.auto, version: QrVersions.auto,
size: 100, size: 100,
gapless: true, gapless: true,
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'), embeddedImage:
AssetImage('assets/icon/icon-light-radius.png'),
embeddedImageStyle: QrEmbeddedImageStyle( embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(28, 28), size: Size(28, 28),
), ),
@@ -572,9 +602,11 @@ class _PostQuestionHint extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20), Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle,
size: 20),
const Gap(4), const Gap(4),
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null) if (data.body['answer'] == null &&
data.body['reward']?.toDouble() != null)
Text('postQuestionUnansweredWithReward'.tr(args: [ Text('postQuestionUnansweredWithReward'.tr(args: [
'${data.body['reward']}', '${data.body['reward']}',
])).opacity(0.75) ])).opacity(0.75)
@@ -610,7 +642,9 @@ class _PostBottomAction extends StatelessWidget {
); );
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key ? data.metric.reactionList.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key
: null; : null;
return Row( return Row(
@@ -624,7 +658,8 @@ class _PostBottomAction extends StatelessWidget {
InkWell( InkWell(
child: Row( child: Row(
children: [ children: [
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null) if (mostTypicalReaction == null ||
kTemplateReactions[mostTypicalReaction] == null)
Icon(Symbols.add_reaction, size: 20, color: iconColor) Icon(Symbols.add_reaction, size: 20, color: iconColor)
else else
Text( Text(
@@ -636,7 +671,8 @@ class _PostBottomAction extends StatelessWidget {
), ),
), ),
const Gap(8), const Gap(8),
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote) if (data.totalUpvote > 0 &&
data.totalUpvote >= data.totalDownvote)
Text('postReactionUpvote').plural( Text('postReactionUpvote').plural(
data.totalUpvote, data.totalUpvote,
) )
@@ -655,8 +691,12 @@ class _PostBottomAction extends StatelessWidget {
data: data, data: data,
onChanged: (value, attr, delta) { onChanged: (value, attr, delta) {
onChanged(data.copyWith( onChanged(data.copyWith(
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote, totalUpvote: attr == 1
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote, ? data.totalUpvote + delta
: data.totalUpvote,
totalDownvote: attr == 2
? data.totalDownvote + delta
: data.totalDownvote,
metric: data.metric.copyWith(reactionList: value), metric: data.metric.copyWith(reactionList: value),
)); ));
}, },
@@ -904,8 +944,10 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4), const Gap(4),
Text( Text(
isRelativeDate isRelativeDate
? RelativeTime(context).format(data.publishedAt ?? data.createdAt) ? RelativeTime(context)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt), .format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm')
.format(data.publishedAt ?? data.createdAt),
).fontSize(13), ).fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
@@ -923,8 +965,10 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4), const Gap(4),
Text( Text(
isRelativeDate isRelativeDate
? RelativeTime(context).format(data.publishedAt ?? data.createdAt) ? RelativeTime(context)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt), .format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm')
.format(data.publishedAt ?? data.createdAt),
).fontSize(13), ).fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
@@ -1107,7 +1151,8 @@ class _PostContentBody extends StatelessWidget {
if (data.body['content'] == null) return const SizedBox.shrink(); if (data.body['content'] == null) return const SizedBox.shrink();
final content = MarkdownTextContent( final content = MarkdownTextContent(
isAutoWarp: data.type == 'story', isAutoWarp: data.type == 'story',
isEnlargeSticker: true, isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
textScaler: isEnlarge ? TextScaler.linear(1.1) : null, textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'], content: data.body['content'],
attachments: data.preload?.attachments, attachments: data.preload?.attachments,
@@ -1156,10 +1201,12 @@ class _PostQuoteContent extends StatelessWidget {
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.type != 'article' && (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),
@@ -1310,7 +1357,9 @@ class _PostTruncatedHint extends StatelessWidget {
const Gap(4), const Gap(4),
Text('postReadEstimate').tr(args: [ Text('postReadEstimate').tr(args: [
'${Duration( '${Duration(
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed, seconds: (data.body['content_length'] as num).toDouble() *
60 ~/
kHumanReadSpeed,
).inSeconds}s', ).inSeconds}s',
]), ]),
], ],
@@ -1349,7 +1398,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
// If this is a answered question, fetch the answer instead // If this is a answered question, fetch the answer instead
if (widget.data.type == 'question' && widget.data.body['answer'] != null) { if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}'); final resp =
await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
_isAnswer = true; _isAnswer = true;
setState(() => _featuredComment = SnPost.fromJson(resp.data)); setState(() => _featuredComment = SnPost.fromJson(resp.data));
return; return;
@@ -1357,9 +1407,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: { final resp = await sn.client.get(
'take': 1, '/cgi/co/posts/${widget.data.id}/replies/featured',
}); queryParameters: {
'take': 1,
});
setState(() => _featuredComment = SnPost.fromJson(resp.data[0])); setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@@ -1388,7 +1440,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
width: double.infinity, width: double.infinity,
child: Material( child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh, color: _isAnswer
? Colors.green.withOpacity(0.5)
: Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell( child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () { onTap: () {
@@ -1408,11 +1462,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const Gap(2), const Gap(2),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20), Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
size: 20),
const Gap(10), const Gap(10),
Text( Text(
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment', _isAnswer
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15), ? 'postQuestionAnswerTitle'
: 'postFeaturedComment',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 15),
).tr(), ).tr(),
], ],
), ),
@@ -1550,7 +1610,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
} }
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>'); RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); setState(
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -1573,11 +1634,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
children: [ children: [
const Icon(Symbols.book_4_spark, size: 24), const Icon(Symbols.book_4_spark, size: 24),
const Gap(16), const Gap(16),
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(), Text('postGetInsightTitle',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
const Gap(4), const Gap(4),
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20), Text('postGetInsightDescription',
style: Theme.of(context).textTheme.bodySmall)
.tr()
.padding(horizontal: 20),
const Gap(4), const Gap(4),
if (_response == null) if (_response == null)
Expanded( Expanded(
@@ -1595,12 +1661,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
leading: const Icon(Symbols.info), leading: const Icon(Symbols.info),
title: Text('aiThinkingProcess'.tr()), title: Text('aiThinkingProcess'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 20), tilePadding: const EdgeInsets.symmetric(horizontal: 20),
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
minTileHeight: 32, minTileHeight: 32,
children: [ children: [
SelectableText( SelectableText(
_thinkingProcess!, _thinkingProcess!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic), style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontStyle: FontStyle.italic),
).padding(horizontal: 20, vertical: 8), ).padding(horizontal: 20, vertical: 8),
], ],
).padding(vertical: 8), ).padding(vertical: 8),
@@ -1637,7 +1707,8 @@ class _PostVideoPlayer extends StatelessWidget {
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'), child: AttachmentItem(
data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
), ),
), ),
); );

View File

@@ -12,6 +12,7 @@
#include <flutter_udid/flutter_udid_plugin.h> #include <flutter_udid/flutter_udid_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h> #include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
#include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h> #include <pasteboard/pasteboard_plugin.h>
@@ -38,6 +39,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);

View File

@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_udid flutter_udid
flutter_webrtc flutter_webrtc
hotkey_manager_linux hotkey_manager_linux
local_notifier
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video
pasteboard pasteboard

View File

@@ -21,6 +21,7 @@ import gal
import hotkey_manager_macos import hotkey_manager_macos
import in_app_review import in_app_review
import livekit_client import livekit_client
import local_notifier
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
import package_info_plus import package_info_plus
@@ -53,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View File

@@ -357,10 +357,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dart_webrtc name: dart_webrtc
sha256: "03df5b41b23bc185ebcf4b0ffc92d002e295bf56287fb5f9d2c321ddaf7760cc" sha256: b34e90bc82f33c1023cf98661369c37bccd648c8a4cf882a875d9f5d8bbef694
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.2+hotfix.1"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -865,10 +865,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: "9c4ca34ced1d1b780baf3776557f9edd0af18ce030969346f752e8df455faaab" sha256: "6ea3a86d95b61cfe42d5715426d355b3cece6c88d0119de428d56f6c653811ce"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.10" version: "0.12.11"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -1201,10 +1201,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52" sha256: "753bbf484c6b70f10f3dc1dc808dfe3755f472d80eb9682323cff07ad8e2609d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.6" version: "2.4.0"
local_notifier:
dependency: "direct main"
description:
name: local_notifier
sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025
url: "https://pub.dev"
source: hosted
version: "0.1.6"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -1549,14 +1557,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.6" version: "3.1.6"
platform_detect:
dependency: transitive
description:
name: platform_detect
sha256: "7394dc1d884e652785a37c3ff25c54e503c6d9fa2f35b55d5efc0a133dec122c"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -2286,10 +2286,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webrtc_interface name: webrtc_interface
sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26" sha256: e05f00091c9c70a15bab4ccb1b6c46d9a16a6075002f02cfac3641eccb05e25d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1+hotfix.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.3.2+70 version: 2.3.2+71
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -123,6 +123,7 @@ dependencies:
image_picker_platform_interface: ^2.10.1 image_picker_platform_interface: ^2.10.1
drift: ^2.25.1 drift: ^2.25.1
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
local_notifier: ^0.1.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -17,6 +17,7 @@
#include <gal/gal_plugin_c_api.h> #include <gal/gal_plugin_c_api.h>
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h> #include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h> #include <livekit_client/live_kit_plugin.h>
#include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h> #include <pasteboard/pasteboard_plugin.h>
@@ -50,6 +51,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
LiveKitPluginRegisterWithRegistrar( LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin")); registry->GetRegistrarForPlugin("LiveKitPlugin"));
LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar( MediaKitVideoPluginCApiRegisterWithRegistrar(

View File

@@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
gal gal
hotkey_manager_windows hotkey_manager_windows
livekit_client livekit_client
local_notifier
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video
pasteboard pasteboard