Compare commits

..

12 Commits

Author SHA1 Message Date
d6013078bd 🚀 Launch 2.4.2+81 2025-03-17 00:38:15 +08:00
5976d61997 💄 Bunch of optimization 2025-03-17 00:36:20 +08:00
b492db90ca 🚀 Launch Feature Drop 2.4.2+80 2025-03-16 23:27:52 +08:00
c9f69fed2c Complete translation 2025-03-16 23:24:36 +08:00
d2f4e7a969 Message translation 2025-03-16 23:10:59 +08:00
aecd04e0b9 Translate infra & post translation 2025-03-16 23:05:07 +08:00
e5212419ae 🐛 Fix poll percentage background 2025-03-16 22:13:19 +08:00
ec7650a920 💄 Optimize nesting 2025-03-16 22:11:40 +08:00
7b96013406 Better(?) comment nesting 2025-03-16 21:41:38 +08:00
fc5a79b29b Blurry attachment background 2025-03-16 19:34:42 +08:00
4146820be5 🐛 Bug fixes 2025-03-16 19:24:21 +08:00
9ec0f1ff19 💄 Redesigned post item 2025-03-16 18:56:08 +08:00
28 changed files with 1877 additions and 1017 deletions

View File

@@ -207,6 +207,7 @@
"one": "{} comment", "one": "{} comment",
"other": "{} comments" "other": "{} comments"
}, },
"postCommentExpand": "Show comments",
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsCustomFonts": "Custom Fonts", "settingsCustomFonts": "Custom Fonts",
"settingsCustomFontsDescription": "Set custom fonts for the application.", "settingsCustomFontsDescription": "Set custom fonts for the application.",
@@ -811,6 +812,8 @@
"accountActionEvent": "Action Events", "accountActionEvent": "Action Events",
"accountActionEventDescription": "View your action event logs.", "accountActionEventDescription": "View your action event logs.",
"eventMetadata": "Metadata", "eventMetadata": "Metadata",
"accountAuthTickets": "Auth Sessions",
"accountAuthTicketsDescription": "View and manage your auth sessions.",
"authTicketCreatedAt": "Issued at {}", "authTicketCreatedAt": "Issued at {}",
"authTicketExpiredAt": "Expired at {}", "authTicketExpiredAt": "Expired at {}",
"authTicketLastGrantAt": "Last granted at {}", "authTicketLastGrantAt": "Last granted at {}",
@@ -837,5 +840,11 @@
"fieldContactContent": "Contact method", "fieldContactContent": "Contact method",
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.", "accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
"accountContactMethodsDelete": "Delete Contact Method", "accountContactMethodsDelete": "Delete Contact Method",
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible." "accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
"postCommentAdd": "Write a comment",
"translate": "Translate",
"translating": "Translating…",
"translated": "Translated",
"settingsAutoTranslate": "Auto Translate",
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages."
} }

View File

@@ -205,6 +205,7 @@
"one": "{} 条评论", "one": "{} 条评论",
"other": "{} 条评论" "other": "{} 条评论"
}, },
"postCommentExpand": "展开评论",
"settingsAppearance": "外观", "settingsAppearance": "外观",
"settingsCustomFonts": "自定义字体", "settingsCustomFonts": "自定义字体",
"settingsCustomFontsDescription": "设置应用程序使用的字体。", "settingsCustomFontsDescription": "设置应用程序使用的字体。",
@@ -837,5 +838,11 @@
"fieldContactContent": "联系方式", "fieldContactContent": "联系方式",
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。", "accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
"accountContactMethodsDelete": "删除联系方式", "accountContactMethodsDelete": "删除联系方式",
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。" "accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
"postCommentAdd": "撰写一条评论",
"translate": "翻译",
"translating": "正在翻译……",
"translated": "已翻译",
"settingsAutoTranslate": "自动翻译",
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。"
} }

View File

@@ -205,6 +205,7 @@
"one": "{} 條評論", "one": "{} 條評論",
"other": "{} 條評論" "other": "{} 條評論"
}, },
"postCommentExpand": "展開評論",
"settingsAppearance": "外觀", "settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體", "settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。", "settingsCustomFontsDescription": "設置應用程序使用的字體。",
@@ -837,5 +838,11 @@
"fieldContactContent": "聯繫方式", "fieldContactContent": "聯繫方式",
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。", "accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
"accountContactMethodsDelete": "刪除聯繫方式", "accountContactMethodsDelete": "刪除聯繫方式",
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。" "accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
"postCommentAdd": "撰寫一條評論",
"translate": "翻譯",
"translating": "正在翻譯……",
"translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
} }

View File

@@ -205,6 +205,7 @@
"one": "{} 條評論", "one": "{} 條評論",
"other": "{} 條評論" "other": "{} 條評論"
}, },
"postCommentExpand": "展開評論",
"settingsAppearance": "外觀", "settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體", "settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。", "settingsCustomFontsDescription": "設置應用程序使用的字體。",
@@ -837,5 +838,11 @@
"fieldContactContent": "聯繫方式", "fieldContactContent": "聯繫方式",
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。", "accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
"accountContactMethodsDelete": "刪除聯繫方式", "accountContactMethodsDelete": "刪除聯繫方式",
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。" "accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
"postCommentAdd": "撰寫一條評論",
"translate": "翻譯",
"translating": "正在翻譯……",
"translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
} }

View File

@@ -37,6 +37,7 @@ import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/sn_sticker.dart'; import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/providers/translation.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
@@ -167,6 +168,7 @@ class SolianApp extends StatelessWidget {
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
Provider(create: (ctx) => SnTranslator()),
// Additional helper layer // Additional helper layer
Provider(create: (ctx) => SpecialDayProvider(ctx)), Provider(create: (ctx) => SpecialDayProvider(ctx)),
@@ -274,7 +276,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
mounted) { mounted) {
final config = context.read<ConfigProvider>(); final config = context.read<ConfigProvider>();
config.setUpdate( config.setUpdate(
remoteVersionString, resp.data?['body'] ?? 'No changelog'); remoteVersionString,
resp.data?['body'] ?? 'No changelog',
);
logging.info("[Update] Update available: $remoteVersionString"); logging.info("[Update] Update available: $remoteVersionString");
} }
} catch (e) { } catch (e) {

View File

@@ -20,6 +20,7 @@ const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view'; const kAppRealmCompactView = 'app_realm_compact_view';
const kAppCustomFonts = 'app_custom_fonts'; const kAppCustomFonts = 'app_custom_fonts';
const kAppMixedFeed = 'app_mixed_feed'; const kAppMixedFeed = 'app_mixed_feed';
const kAppAutoTranslate = 'app_auto_translate';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@@ -86,6 +87,15 @@ class ConfigProvider extends ChangeNotifier {
return prefs.getBool(kAppMixedFeed) ?? true; return prefs.getBool(kAppMixedFeed) ?? true;
} }
bool get autoTranslate {
return prefs.getBool(kAppAutoTranslate) ?? false;
}
set autoTranslate(bool value) {
prefs.setBool(kAppAutoTranslate, value);
notifyListeners();
}
set mixedFeed(bool value) { set mixedFeed(bool value) {
prefs.setBool(kAppMixedFeed, value); prefs.setBool(kAppMixedFeed, value);
notifyListeners(); notifyListeners();

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:surface/logger.dart';
// TODO self host translate api
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
class SnTranslator {
final Dio client = Dio(
BaseOptions(
baseUrl: kTranslateApiBaseUrl,
connectTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
receiveTimeout: Duration(seconds: 3),
),
);
final Map<String, String> _cache = {};
Future<String> translate(
String text, {
required String to,
String from = 'auto',
bool skipCache = false,
}) async {
if (text.isEmpty) return text;
final cacheKey = md5.convert(utf8.encode('$text$from$to')).toString();
if (!skipCache && _cache.containsKey(cacheKey)) {
return _cache[cacheKey]!;
}
logging.info('[Translator] Translate $text from $from to $to');
final resp = await client.post(
'/translate',
data: {
'q': text,
'source': from,
'target': to,
'format': 'text',
},
);
if (resp.statusCode == 200) {
final out = resp.data['translatedText'];
if (out.isNotEmpty) {
logging.info('[Translator] Translated $text from $from to $to');
_cache[cacheKey] = out;
return out;
}
}
throw Exception('translate failed: $resp');
}
}

View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountNotifyPrefsScreen extends StatelessWidget {
const AccountNotifyPrefsScreen({super.key});
@override
Widget build(BuildContext context) {
return AppScaffold();
}
}

View File

@@ -551,12 +551,20 @@ class _PostListWidgetState extends State<_PostListWidget> {
maxWidth: 640, maxWidth: 640,
); );
case 'reader.news': case 'reader.news':
return NewsFeedEntry(data: ele); return Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: NewsFeedEntry(data: ele),
),
);
default: default:
return FeedUnknownEntry(data: ele); return Container(
constraints: BoxConstraints(maxWidth: 640),
child: FeedUnknownEntry(data: ele),
);
} }
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
), ),
), ),
); );

View File

@@ -389,7 +389,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
size: 20, size: 20,
), ),
const Gap(10), const Gap(10),
Text('serviceStatusOperational').tr(), Text('loading').tr(),
], ],
) )
: switch (_serviceStatus) { : switch (_serviceStatus) {
@@ -434,6 +434,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
padding: EdgeInsets.only(top: 6), padding: EdgeInsets.only(top: 6),
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
runSpacing: 8,
children: [ children: [
for (final entry in _statuses!.entries) for (final entry in _statuses!.entries)
Tooltip( Tooltip(
@@ -441,6 +442,8 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
? 'serviceName${kServicesName[entry.key]}'.tr() ? 'serviceName${kServicesName[entry.key]}'.tr()
: 'unknown'.tr(), : 'unknown'.tr(),
child: Chip( child: Chip(
visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
avatar: entry.value avatar: entry.value
? const Icon( ? const Icon(
Symbols.circle, Symbols.circle,
@@ -877,8 +880,10 @@ class _HomeDashRecommendationPostWidgetState
).tr(), ).tr(),
], ],
), ),
Text('${_currentPage + 1}/${_posts?.length ?? 0}', Text(
style: GoogleFonts.robotoMono()) '${_currentPage + 1}/${_posts?.length ?? 0}',
style: GoogleFonts.robotoMono(),
)
], ],
).padding(horizontal: 18, top: 12, bottom: 8), ).padding(horizontal: 18, top: 12, bottom: 8),
Expanded( Expanded(
@@ -896,6 +901,7 @@ class _HomeDashRecommendationPostWidgetState
child: PostItem( child: PostItem(
data: _posts![index], data: _posts![index],
showMenu: false, showMenu: false,
showFullPost: true,
).padding(bottom: 8), ).padding(bottom: 8),
onTap: () { onTap: () {
GoRouter.of(context) GoRouter.of(context)

View File

@@ -63,7 +63,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
queryParameters: {'take': 10, 'offset': _notifications.length}, queryParameters: {'take': 10, 'offset': _notifications.length},
); );
_totalCount = resp.data['count']; _totalCount = resp.data['count'];
_notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []); _notifications.addAll(resp.data['data']
?.map((e) => SnNotification.fromJson(e))
.cast<SnNotification>() ??
[]);
nty.updateTray(); nty.updateTray();
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@@ -98,7 +101,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
nty.clear(); nty.clear();
if (!mounted) return; if (!mounted) return;
context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count'])); context.showSnackbar(
'notificationMarkAllReadPrompt'.plural(resp.data['count']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -122,7 +126,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
_fetchNotifications(); _fetchNotifications();
if (!mounted) return; if (!mounted) return;
context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}'])); context.showSnackbar(
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -143,7 +148,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()), appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenNotification').tr()),
body: Center(child: UnauthorizedHint()), body: Center(child: UnauthorizedHint()),
); );
} }
@@ -153,7 +160,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),
actions: [ actions: [
IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead), IconButton(
icon: const Icon(Symbols.checklist),
onPressed: _isSubmitting ? null : _markAllAsRead),
const Gap(8), const Gap(8),
], ],
), ),
@@ -167,13 +176,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
return _fetchNotifications(); return _fetchNotifications();
}, },
child: InfiniteList( child: InfiniteList(
padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)), padding: EdgeInsets.only(
top: 16,
bottom:
math.max(MediaQuery.of(context).padding.bottom, 16)),
itemCount: _notifications.length, itemCount: _notifications.length,
onFetchData: () { onFetchData: () {
_fetchNotifications(); _fetchNotifications();
}, },
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!, hasReachedMax: _totalCount != null &&
_notifications.length >= _totalCount!,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final nty = _notifications[idx]; final nty = _notifications[idx];
return Row( return Row(
@@ -186,12 +199,19 @@ class _NotificationScreenState extends State<NotificationScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (nty.readAt == null) if (nty.readAt == null)
StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4), StyledWidget(Badge(
Text(nty.title, style: Theme.of(context).textTheme.titleMedium), label: Text('notificationUnread').tr()))
.padding(bottom: 4),
Text(nty.title,
style: Theme.of(context).textTheme.titleMedium),
if (nty.subtitle != null) if (nty.subtitle != null)
Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall), Text(nty.subtitle!,
style:
Theme.of(context).textTheme.titleSmall),
if (nty.subtitle != null) const Gap(4), if (nty.subtitle != null) const Gap(4),
SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)), SelectionArea(
child: MarkdownTextContent(
content: nty.body, isAutoWarp: true)),
if ([ if ([
'interactive.reply', 'interactive.reply',
'interactive.feedback', 'interactive.feedback',
@@ -201,31 +221,43 @@ class _NotificationScreenState extends State<NotificationScreen> {
GestureDetector( GestureDetector(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(
border: Border.all(color: Theme.of(context).dividerColor, width: 1), Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1),
), ),
child: PostItem( child: PostItem(
data: SnPost.fromJson(nty.metadata['related_post']!), data: SnPost.fromJson(
nty.metadata['related_post']!),
showComments: false, showComments: false,
showReactions: false, showReactions: false,
showMenu: false, showMenu: false,
), ).padding(vertical: 4),
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postDetail', 'postDetail',
pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()}, pathParameters: {
'slug': nty
.metadata['related_post']!['id']
.toString()
},
); );
}, },
).padding(top: 8), ).padding(top: 8),
const Gap(8), const Gap(8),
Row( Row(
children: [ children: [
Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12), Text(DateFormat('yy/MM/dd')
.format(nty.createdAt))
.fontSize(12),
const Gap(4), const Gap(4),
Text('·', style: TextStyle(fontSize: 12)), Text('·', style: TextStyle(fontSize: 12)),
const Gap(4), const Gap(4),
Text(RelativeTime(context).format(nty.createdAt)).fontSize(12), Text(RelativeTime(context)
.format(nty.createdAt))
.fontSize(12),
], ],
).opacity(0.75), ).opacity(0.75),
], ],
@@ -235,8 +267,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
IconButton( IconButton(
icon: const Icon(Symbols.check), icon: const Icon(Symbols.check),
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4), visualDensity:
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty), const VisualDensity(horizontal: -4, vertical: -4),
onPressed:
_isSubmitting ? null : () => _markOneAsRead(nty),
), ),
], ],
).padding(horizontal: 16); ).padding(horizontal: 16);

View File

@@ -22,7 +22,8 @@ class PostDetailScreen extends StatefulWidget {
final SnPost? preload; final SnPost? preload;
final Function? onBack; final Function? onBack;
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack}); const PostDetailScreen(
{super.key, required this.slug, this.preload, this.onBack});
@override @override
State<PostDetailScreen> createState() => _PostDetailScreenState(); State<PostDetailScreen> createState() => _PostDetailScreenState();
@@ -88,14 +89,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
TextSpan( TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(), text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!, 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( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!, color:
Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
]), ]),
@@ -124,8 +127,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
}, },
), ),
), ),
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)), if (_data != null)
if (_data != null && _data!.type != 'video') SliverToBoxAdapter(
child: Divider(height: 1).padding(top: 8),
),
if (_data != null)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth),
@@ -141,7 +147,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
).padding(horizontal: 20, vertical: 12).center(), ).padding(horizontal: 20, vertical: 12).center(),
), ),
), ),
if (_data != null && ua.isAuthorized && _data!.type != 'video') if (_data != null && ua.isAuthorized)
SliverToBoxAdapter( SliverToBoxAdapter(
child: PostCommentQuickAction( child: PostCommentQuickAction(
parentPost: _data!, parentPost: _data!,
@@ -158,13 +164,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
}, },
), ),
), ),
if (_data != null && _data!.type != 'video') if (_data != null) SliverGap(8),
if (_data != null)
PostCommentSliverList( PostCommentSliverList(
key: _childListKey, key: _childListKey,
parentPost: _data!, parentPost: _data!,
maxWidth: maxWidth, maxWidth: maxWidth,
), ),
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), if (_data != null)
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
], ],
), ),
), ),

View File

@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@@ -77,7 +77,8 @@ class _PostDraftBoxState extends State<PostDraftBox> {
}, },
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, __) =>
const Divider().padding(vertical: 2),
), ),
), ),
), ),

View File

@@ -45,7 +45,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
} }
Future<void> _fetchPosts() async { Future<void> _fetchPosts() async {
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return; if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty)
return;
if (_postCount != null && _posts.length >= _postCount!) return; if (_postCount != null && _posts.length >= _postCount!) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -152,7 +153,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
}, },
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
), ),
Positioned( Positioned(
top: 16, top: 16,
@@ -166,7 +167,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
padding: const WidgetStatePropertyAll( padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 24), EdgeInsets.symmetric(horizontal: 24),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (value) { onChanged: (value) {
_searchTerm = value; _searchTerm = value;
}, },

View File

@@ -34,9 +34,11 @@ class PostPublisherScreen extends StatefulWidget {
State<PostPublisherScreen> createState() => _PostPublisherScreenState(); State<PostPublisherScreen> createState() => _PostPublisherScreenState();
} }
class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTickerProviderStateMixin { class _PostPublisherScreenState extends State<PostPublisherScreen>
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
late final TabController _tabController = TabController(length: 3, vsync: this); late final TabController _tabController =
TabController(length: 3, vsync: this);
SnPublisher? _publisher; SnPublisher? _publisher;
SnAccount? _account; SnAccount? _account;
@@ -66,7 +68,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
_account = await ud.getAccount(_publisher?.accountId); _account = await ud.getAccount(_publisher?.accountId);
_accountRelationship = await rel.getRelationship(_account!.id); _accountRelationship = await rel.getRelationship(_account!.id);
if (_publisher?.realmId != null && _publisher!.realmId != 0) { if (_publisher?.realmId != null && _publisher!.realmId != 0) {
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}'); final resp =
await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
_realm = SnRealm.fromJson(resp.data); _realm = SnRealm.fromJson(resp.data);
} }
} catch (_) { } catch (_) {
@@ -133,12 +136,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
double _appBarBlur = 0.0; double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble(); late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
setState(() { setState(() {
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); _appBarBlur =
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
}); });
} }
@@ -193,7 +198,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
'related': _account!.name, 'related': _account!.name,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}'])); context.showSnackbar(
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -209,9 +215,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
try { try {
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;
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}'])); context.showSnackbar(
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -299,7 +307,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
text: TextSpan(children: [ text: TextSpan(children: [
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: Colors.white,
shadows: labelShadows, shadows: labelShadows,
), ),
@@ -307,7 +318,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: '@${_publisher!.name}', text: '@${_publisher!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
color: Colors.white, color: Colors.white,
shadows: labelShadows, shadows: labelShadows,
), ),
@@ -330,13 +344,16 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
) )
else else
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context)
.colorScheme
.surfaceContainer,
), ),
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
height: 56 + MediaQuery.of(context).padding.top, height:
56 + MediaQuery.of(context).padding.top,
child: ClipRect( child: ClipRect(
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
@@ -345,7 +362,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
), ),
child: Container( child: Container(
color: Colors.black.withOpacity( color: Colors.black.withOpacity(
clampDouble(_appBarBlur * 0.1, 0, 0.5), clampDouble(
_appBarBlur * 0.1, 0, 0.5),
), ),
), ),
), ),
@@ -372,11 +390,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
const Gap(16), const Gap(16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_publisher!.nick, _publisher!.nick,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context)
.textTheme
.titleMedium,
).bold(), ).bold(),
Text('@${_publisher!.name}').fontSize(13), Text('@${_publisher!.name}').fontSize(13),
], ],
@@ -387,7 +408,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
style: ButtonStyle( style: ButtonStyle(
elevation: WidgetStatePropertyAll(0), elevation: WidgetStatePropertyAll(0),
), ),
onPressed: _isSubscribing ? null : _toggleSubscription, onPressed: _isSubscribing
? null
: _toggleSubscription,
label: Text('subscribe').tr(), label: Text('subscribe').tr(),
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
) )
@@ -396,14 +419,17 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
style: ButtonStyle( style: ButtonStyle(
elevation: WidgetStatePropertyAll(0), elevation: WidgetStatePropertyAll(0),
), ),
onPressed: _isSubscribing ? null : _toggleSubscription, onPressed: _isSubscribing
? null
: _toggleSubscription,
label: Text('unsubscribe').tr(), label: Text('unsubscribe').tr(),
icon: const Icon(Symbols.remove), icon: const Icon(Symbols.remove),
), ),
PopupMenuButton( PopupMenuButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: ButtonStyle( style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity: VisualDensity(
horizontal: -4, vertical: -4),
), ),
itemBuilder: (BuildContext context) => [ itemBuilder: (BuildContext context) => [
PopupMenuItem( PopupMenuItem(
@@ -443,7 +469,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
], ],
), ),
const Gap(12), const Gap(12),
Text(_publisher!.description).padding(horizontal: 8), Text(_publisher!.description)
.padding(horizontal: 8),
const Gap(12), const Gap(12),
Column( Column(
children: [ children: [
@@ -451,8 +478,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
children: [ children: [
const Icon(Symbols.calendar_add_on), const Icon(Symbols.calendar_add_on),
const Gap(8), const Gap(8),
Text('publisherJoinedAt') Text('publisherJoinedAt').tr(args: [
.tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]), DateFormat('y/M/d')
.format(_publisher!.createdAt)
]),
], ],
), ),
Row( Row(
@@ -460,7 +489,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
const Icon(Symbols.trending_up), const Icon(Symbols.trending_up),
const Gap(8), const Gap(8),
Text('publisherSocialPointTotal').plural( Text('publisherSocialPointTotal').plural(
_publisher!.totalUpvote - _publisher!.totalDownvote, _publisher!.totalUpvote -
_publisher!.totalDownvote,
), ),
], ],
), ),
@@ -470,18 +500,22 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
const Icon(Symbols.group_work), const Icon(Symbols.group_work),
const Gap(8), const Gap(8),
InkWell( InkWell(
child: Text('publisherAffiliatedBy').tr(args: [ child: Text('publisherAffiliatedBy')
.tr(args: [
'@${_realm?.alias ?? 'unknown'}', '@${_realm?.alias ?? 'unknown'}',
]), ]),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'realmDetail', 'realmDetail',
pathParameters: {'alias': _realm!.alias}, pathParameters: {
'alias': _realm!.alias
},
); );
}, },
), ),
const Gap(8), const Gap(8),
AccountImage(content: _realm?.avatar, radius: 8), AccountImage(
content: _realm?.avatar, radius: 8),
], ],
), ),
Row( Row(
@@ -502,7 +536,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
}, },
), ),
const Gap(8), const Gap(8),
AccountImage(content: _account?.avatar, radius: 8), AccountImage(
content: _account?.avatar, radius: 8),
], ],
), ),
], ],
@@ -606,7 +641,7 @@ class _PublisherPostList extends StatelessWidget {
onDeleted: onDeleted, onDeleted: onDeleted,
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
); );
} }
} }

View File

@@ -387,6 +387,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
.fontSize(17) .fontSize(17)
.tr() .tr()
.padding(horizontal: 20, bottom: 4), .padding(horizontal: 20, bottom: 4),
CheckboxListTile(
secondary: const Icon(Symbols.translate),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('settingsAutoTranslate').tr(),
subtitle: Text('settingsAutoTranslateDescription').tr(),
value: _prefs.getBool(kAppAutoTranslate) ?? false,
onChanged: (value) {
setState(() {
_prefs.setBool(kAppAutoTranslate, value ?? false);
});
},
),
CheckboxListTile( CheckboxListTile(
secondary: const Icon(Symbols.vibration), secondary: const Icon(Symbols.vibration),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),

View File

@@ -14,6 +14,7 @@ class AccountImage extends StatelessWidget {
final Widget? fallbackWidget; final Widget? fallbackWidget;
final Widget? badge; final Widget? badge;
final Offset? badgeOffset; final Offset? badgeOffset;
final FilterQuality? filterQuality;
const AccountImage({ const AccountImage({
super.key, super.key,
@@ -25,6 +26,7 @@ class AccountImage extends StatelessWidget {
this.fallbackWidget, this.fallbackWidget,
this.badge, this.badge,
this.badgeOffset, this.badgeOffset,
this.filterQuality,
}); });
@override @override
@@ -54,6 +56,7 @@ class AccountImage extends StatelessWidget {
) )
: AutoResizeUniversalImage( : AutoResizeUniversalImage(
sn.getAttachmentUrl(url), sn.getAttachmentUrl(url),
filterQuality: filterQuality,
key: Key('attachment-${content.hashCode}'), key: Key('attachment-${content.hashCode}'),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),

View File

@@ -45,11 +45,25 @@ class AttachmentItem extends StatelessWidget {
case 'image': case 'image':
return Hero( return Hero(
tag: 'attachment-${data!.rid}-$tag', tag: 'attachment-${data!.rid}-$tag',
child: AutoResizeUniversalImage( child: Stack(
sn.getAttachmentUrl(data!.rid), fit: StackFit.expand,
key: Key('attachment-${data!.rid}-$tag'), children: [
fit: fit, ImageFiltered(
filterQuality: filterQuality, imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data!.rid),
key: Key('attachment-${data!.rid}-$tag-blur-background'),
fit: BoxFit.cover,
filterQuality: filterQuality,
),
),
AutoResizeUniversalImage(
sn.getAttachmentUrl(data!.rid),
key: Key('attachment-${data!.rid}-$tag'),
fit: fit,
filterQuality: filterQuality,
),
],
), ),
); );
case 'video': case 'video':

View File

@@ -10,6 +10,7 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/keypair.dart'; import 'package:surface/providers/keypair.dart';
import 'package:surface/providers/translation.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
@@ -18,6 +19,7 @@ import 'package:surface/widgets/account/account_popover.dart';
import 'package:surface/widgets/account/badge.dart'; import 'package:surface/widgets/account/badge.dart';
import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/context_menu.dart'; import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/link_preview.dart'; import 'package:surface/widgets/link_preview.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@@ -228,7 +230,7 @@ class ChatMessage extends StatelessWidget {
} }
} }
class _ChatMessageText extends StatelessWidget { class _ChatMessageText extends StatefulWidget {
final SnChatMessage data; final SnChatMessage data;
final Function(SnChatMessage)? onReply; final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit; final Function(SnChatMessage)? onEdit;
@@ -237,13 +239,56 @@ class _ChatMessageText extends StatelessWidget {
const _ChatMessageText( const _ChatMessageText(
{required this.data, this.onReply, this.onEdit, this.onDelete}); {required this.data, this.onReply, this.onEdit, this.onDelete});
@override
State<_ChatMessageText> createState() => _ChatMessageTextState();
}
class _ChatMessageTextState extends State<_ChatMessageText> {
late String _displayText = widget.data.body['text'] ?? '';
bool _isTranslated = false;
bool _isTranslating = false;
Future<void> _translateText() async {
if (widget.data.body['text'] == null || widget.data.body['text']!.isEmpty) {
return;
}
final ta = context.read<SnTranslator>();
setState(() => _isTranslating = true);
try {
final to = EasyLocalization.of(context)!.locale.languageCode;
_displayText = await ta.translate(
widget.data.body['text'],
to: to,
);
_isTranslated = true;
if (mounted) setState(() {});
} catch (err) {
if (mounted) context.showErrorDialog(err);
} finally {
setState(() => _isTranslating = false);
}
}
@override
void initState() {
super.initState();
final cfg = context.read<ConfigProvider>();
if (cfg.autoTranslate) {
Future.delayed(const Duration(milliseconds: 100), () {
_translateText();
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id; final isOwner =
ua.isAuthorized && widget.data.sender.accountId == ua.user?.id;
if (data.body['text'] != null && data.body['text'].isNotEmpty) { if (widget.data.body['text'] != null &&
widget.data.body['text'].isNotEmpty) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -252,38 +297,50 @@ class _ChatMessageText extends StatelessWidget {
final List<ContextMenuButtonItem> items = final List<ContextMenuButtonItem> items =
editableTextState.contextMenuButtonItems; editableTextState.contextMenuButtonItems;
if (onReply != null) { if (widget.onReply != null) {
items.insert( items.insert(
0, 0,
ContextMenuButtonItem( ContextMenuButtonItem(
label: 'reply'.tr(), label: 'reply'.tr(),
onPressed: () { onPressed: () {
ContextMenuController.removeAny(); ContextMenuController.removeAny();
onReply?.call(data); widget.onReply?.call(widget.data);
}, },
), ),
); );
} }
if (isOwner && onEdit != null) { if (isOwner && widget.onEdit != null) {
items.insert( items.insert(
1, 1,
ContextMenuButtonItem( ContextMenuButtonItem(
label: 'edit'.tr(), label: 'edit'.tr(),
onPressed: () { onPressed: () {
ContextMenuController.removeAny(); ContextMenuController.removeAny();
onEdit?.call(data); widget.onEdit?.call(widget.data);
}, },
), ),
); );
} }
if (isOwner && onDelete != null) { if (isOwner && widget.onDelete != null) {
items.insert( items.insert(
2, 2,
ContextMenuButtonItem( ContextMenuButtonItem(
label: 'delete'.tr(), label: 'delete'.tr(),
onPressed: () { onPressed: () {
ContextMenuController.removeAny(); ContextMenuController.removeAny();
onDelete?.call(data); widget.onDelete?.call(widget.data);
},
),
);
}
if (widget.data.body['algorithm'] == 'plain') {
items.insert(
3,
ContextMenuButtonItem(
label: 'translate'.tr(),
onPressed: () {
ContextMenuController.removeAny();
_translateText();
}, },
), ),
); );
@@ -294,26 +351,47 @@ class _ChatMessageText extends StatelessWidget {
buttonItems: items, buttonItems: items,
); );
}, },
child: switch (data.body['algorithm']) { child: switch (widget.data.body['algorithm']) {
'rsa' => _ChatDecryptMessage(message: data), 'rsa' => _ChatDecryptMessage(message: widget.data),
_ => MarkdownTextContent( _ => MarkdownTextContent(
content: data.body['text'], content: _displayText,
isAutoWarp: true, isAutoWarp: true,
isEnlargeSticker: isEnlargeSticker: RegExp(r"^:([-\w]+):$")
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''), .hasMatch(widget.data.body['text'] ?? ''),
), ),
}, },
), ),
if (data.updatedAt != data.createdAt) if (widget.data.updatedAt != widget.data.createdAt)
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75), Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
if (_isTranslating)
AnimateWidgetExtensions(Text('translating').tr())
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
if (_isTranslated)
InkWell(
child: Text('translated').tr().opacity(0.75),
onTap: () {
setState(() {
_displayText = widget.data.body['text'] ?? '';
_isTranslated = false;
});
},
),
], ],
); );
} else if (data.body['attachments']?.isNotEmpty) { } else if (widget.data.body['attachments']?.isNotEmpty) {
return Row( return Row(
children: [ children: [
const Icon(Symbols.file_present, size: 20), const Icon(Symbols.file_present, size: 20),
const Gap(4), const Gap(4),
Text('messageFileHint'.plural(data.body['attachments']!.length)), Text('messageFileHint'
.plural(widget.data.body['attachments']!.length)),
], ],
).opacity(0.8); ).opacity(0.8);
} }

View File

@@ -19,89 +19,87 @@ class NewsFeedEntry extends StatelessWidget {
.cast<SnNewsArticle>() .cast<SnNewsArticle>()
.toList(); .toList();
return Card( return Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( children: [
children: [ const Icon(Symbols.newspaper),
const Icon(Symbols.newspaper), const Gap(8),
const Gap(8), Text(
Text( 'newsToday',
'newsToday', style: Theme.of(context).textTheme.titleLarge,
style: Theme.of(context).textTheme.titleLarge, ).tr()
).tr() ],
], ).padding(horizontal: 18, top: 12, bottom: 8),
).padding(horizontal: 18, top: 12, bottom: 8), Container(
Container( margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 12), height: 150,
height: 150, child: ListView.separated(
child: ListView.separated( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, itemCount: news.length,
itemCount: news.length, padding: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 12), itemBuilder: (context, idx) {
itemBuilder: (context, idx) { return Container(
return Container( width: 360,
width: 360, decoration: BoxDecoration(
decoration: BoxDecoration( border: Border.all(
border: Border.all( color: Theme.of(context).dividerColor,
color: Theme.of(context).dividerColor, width: 1,
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
), ),
child: Material( borderRadius: const BorderRadius.all(Radius.circular(8)),
elevation: 0, ),
color: Theme.of(context).colorScheme.surface, child: Material(
elevation: 0,
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InkWell( child: Column(
borderRadius: const BorderRadius.all(Radius.circular(8)), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ news[idx].title,
Text( maxLines: 2,
news[idx].title, style: Theme.of(context).textTheme.titleMedium,
maxLines: 2, ).padding(horizontal: 16, top: 12, bottom: 4),
style: Theme.of(context).textTheme.titleMedium, Text(
).padding(horizontal: 16, top: 12, bottom: 4), news[idx].description,
Text( maxLines: 2,
news[idx].description, style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2, ).padding(horizontal: 16, vertical: 4),
style: Theme.of(context).textTheme.bodyMedium, const Gap(4),
).padding(horizontal: 16, vertical: 4), Row(
const Gap(4), children: [
Row( Text(
children: [ DateFormat('y/M/d HH:mm')
Text( .format(news[idx].createdAt.toLocal()),
DateFormat('y/M/d HH:mm') style: Theme.of(context).textTheme.bodySmall,
.format(news[idx].createdAt.toLocal()), ),
style: Theme.of(context).textTheme.bodySmall, const Gap(4),
), Text(
const Gap(4), RelativeTime(context)
Text( .format(news[idx].createdAt.toLocal()),
RelativeTime(context) style: Theme.of(context).textTheme.bodySmall,
.format(news[idx].createdAt.toLocal()), ),
style: Theme.of(context).textTheme.bodySmall, ],
), ).opacity(0.8).padding(horizontal: 16),
], ],
).opacity(0.8).padding(horizontal: 16),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'newsDetail',
pathParameters: {'hash': news[idx].hash},
);
},
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'newsDetail',
pathParameters: {'hash': news[idx].hash},
);
},
), ),
); ),
}, );
separatorBuilder: (_, __) => const Gap(12), },
), separatorBuilder: (_, __) => const Gap(12),
), ),
], ),
), ],
); );
} }
} }

View File

@@ -12,16 +12,14 @@ class FeedUnknownEntry extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ const Icon(Symbols.help, size: 36),
const Icon(Symbols.help, size: 36), const Gap(4),
const Gap(4), Text('feedUnknownItem').tr(),
Text('feedUnknownItem').tr(), Text(data.type, style: GoogleFonts.robotoMono()),
Text(data.type, style: GoogleFonts.robotoMono()), ],
], ).padding(horizontal: 12, vertical: 8);
).padding(horizontal: 12, vertical: 8),
);
} }
} }

View File

@@ -23,57 +23,54 @@ class FediversePostWidget extends StatelessWidget {
return Center( return Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth),
child: Card( child: Column(
margin: EdgeInsets.zero, crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
Row( AccountImage(
children: [ content: data.user.avatar,
AccountImage( radius: 20,
content: data.user.avatar,
radius: 20,
),
const Gap(12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.user.nick.isNotEmpty
? data.user.nick
: '@${data.user.name}',
maxLines: 1,
).bold(),
Row(
children: [
Text(
data.user.identifier.contains('@')
? data.user.identifier
: '${data.user.identifier}@${data.user.origin}',
maxLines: 1,
).fontSize(13),
const Gap(4),
Text(
RelativeTime(context)
.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
],
),
],
).padding(horizontal: 12, vertical: 8),
MarkdownTextContent(
isAutoWarp: true,
content: html2md.convert(data.content),
).padding(horizontal: 16, bottom: 6),
if (data.images.isNotEmpty)
_FediversePostImageList(
data: data,
maxWidth: maxWidth,
), ),
], const Gap(12),
), Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.user.nick.isNotEmpty
? data.user.nick
: '@${data.user.name}',
maxLines: 1,
).bold(),
Row(
children: [
Text(
data.user.identifier.contains('@')
? data.user.identifier
: '${data.user.identifier}@${data.user.origin}',
maxLines: 1,
).fontSize(13),
const Gap(4),
Text(
RelativeTime(context)
.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
],
),
],
).padding(horizontal: 12, vertical: 8),
MarkdownTextContent(
isAutoWarp: true,
content: html2md.convert(data.content),
).padding(horizontal: 16, bottom: 6),
if (data.images.isNotEmpty)
_FediversePostImageList(
data: data,
maxWidth: maxWidth,
),
],
), ),
), ),
); );

View File

@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@@ -14,14 +15,13 @@ import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_mini_editor.dart'; import 'package:surface/widgets/post/post_mini_editor.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/sn_network.dart';
class PostCommentQuickAction extends StatelessWidget { class PostCommentQuickAction extends StatelessWidget {
final double? maxWidth; final double? maxWidth;
final SnPost parentPost; final SnPost parentPost;
final Function? onPosted; final Function? onPosted;
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted}); const PostCommentQuickAction(
{super.key, this.maxWidth, required this.parentPost, this.onPosted});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -30,7 +30,9 @@ class PostCommentQuickAction extends StatelessWidget {
return Container( return Container(
height: 240, height: 240,
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 8) : EdgeInsets.zero, margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const EdgeInsets.symmetric(vertical: 8)
: EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const BorderRadius.all(Radius.circular(8)) ? const BorderRadius.all(Radius.circular(8))
@@ -99,7 +101,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
Future<void> _selectAnswer(SnPost answer) async { Future<void> _selectAnswer(SnPost answer) async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: { await sn.client
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
'publisher': answer.publisherId, 'publisher': answer.publisherId,
'answer_id': answer.id, 'answer_id': answer.id,
}); });
@@ -135,7 +138,10 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
child: PostItem( child: PostItem(
data: _posts[idx], data: _posts[idx],
maxWidth: widget.maxWidth, maxWidth: widget.maxWidth,
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null, showExpandableComments: true,
onSelectAnswer: widget.parentPost.type == 'question'
? () => _selectAnswer(_posts[idx])
: null,
onChanged: (data) { onChanged: (data) {
setState(() => _posts[idx] = data); setState(() => _posts[idx] = data);
}, },
@@ -153,7 +159,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
}, },
); );
}, },
separatorBuilder: (context, index) => const Divider(height: 1), separatorBuilder: (context, index) =>
const Divider().padding(vertical: 2),
); );
} }
} }
@@ -161,11 +168,13 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
class PostCommentListPopup extends StatefulWidget { class PostCommentListPopup extends StatefulWidget {
final SnPost post; final SnPost post;
final int commentCount; final int commentCount;
final int depth;
const PostCommentListPopup({ const PostCommentListPopup({
super.key, super.key,
required this.post, required this.post,
this.commentCount = 0, this.commentCount = 0,
this.depth = 1,
}); });
@override @override
@@ -180,48 +189,54 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Column( return SizedBox(
crossAxisAlignment: CrossAxisAlignment.start, height: MediaQuery.of(context).size.height * 0.85,
children: [ child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ Row(
const Icon(Symbols.comment, size: 24), crossAxisAlignment: CrossAxisAlignment.center,
const Gap(16), children: [
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!), const Icon(Symbols.comment, size: 24),
], const Gap(16),
).padding(horizontal: 20, top: 16, bottom: 12), Text('postCommentsDetailed')
Expanded( .plural(widget.commentCount)
child: CustomScrollView( .textStyle(Theme.of(context).textTheme.titleLarge!),
slivers: [ ],
if (ua.isAuthorized) ).padding(horizontal: 20, top: 16, bottom: 12),
SliverToBoxAdapter( Expanded(
child: Container( child: CustomScrollView(
height: 240, slivers: [
decoration: BoxDecoration( if (ua.isAuthorized)
border: Border.symmetric( SliverToBoxAdapter(
horizontal: BorderSide( child: Container(
color: Theme.of(context).dividerColor, margin: const EdgeInsets.only(bottom: 8),
width: 1 / devicePixelRatio, height: 240,
decoration: BoxDecoration(
border: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
), ),
), ),
), child: PostMiniEditor(
child: PostMiniEditor( postReplyId: widget.post.id,
postReplyId: widget.post.id, onPost: () {
onPost: () { _childListKey.currentState!.refresh();
_childListKey.currentState!.refresh(); },
}, ),
), ),
), ),
PostCommentSliverList(
parentPost: widget.post,
key: _childListKey,
), ),
PostCommentSliverList( ],
parentPost: widget.post, ),
key: _childListKey,
),
],
), ),
), ],
], ),
); );
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -80,59 +80,64 @@ class _PostPollState extends State<PostPoll> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return LayoutBuilder(
margin: EdgeInsets.zero, builder: (context, constraints) {
child: Column( return Card(
children: [ margin: EdgeInsets.zero,
for (final option in _poll.options) child: Column(
Stack( children: [
children: [ for (final option in _poll.options)
ClipRRect( Stack(
borderRadius: const BorderRadius.all(Radius.circular(8)), children: [
child: Container( ClipRRect(
height: 60, borderRadius: const BorderRadius.all(Radius.circular(8)),
width: MediaQuery.of(context).size.width * child: Container(
(_poll.metric.byOptionsPercentage[option.id] ?? 0) height: 60,
.toDouble(), width: constraints.maxWidth *
color: Theme.of(context).colorScheme.surfaceContainerHigh, (_poll.metric.byOptionsPercentage[option.id] ?? 0)
), .toDouble(),
), color:
ListTile( Theme.of(context).colorScheme.surfaceContainerHigh,
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(8), ),
), ListTile(
minTileHeight: 60, shape: RoundedRectangleBorder(
leading: _answeredChoice == option.id borderRadius: BorderRadius.circular(8),
? const Icon(Symbols.circle, fill: 1) ),
: const Icon(Symbols.circle), minTileHeight: 60,
title: Text(option.name), leading: _answeredChoice == option.id
subtitle: Column( ? const Icon(Symbols.circle, fill: 1)
crossAxisAlignment: CrossAxisAlignment.start, : const Icon(Symbols.circle),
mainAxisSize: MainAxisSize.min, title: Text(option.name),
children: [ subtitle: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Row(
'pollVotes' mainAxisSize: MainAxisSize.min,
.plural(_poll.metric.byOptions[option.id] ?? 0), children: [
), Text(
Text(' · ').padding(horizontal: 4), 'pollVotes'.plural(
Text( _poll.metric.byOptions[option.id] ?? 0),
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%', ),
Text(' · ').padding(horizontal: 4),
Text(
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
),
],
), ),
if (option.description.isNotEmpty)
Text(option.description),
], ],
), ),
if (option.description.isNotEmpty) onTap: _isBusy ? null : () => _voteForOption(option),
Text(option.description), ),
], ],
), )
onTap: _isBusy ? null : () => _voteForOption(option), ],
), ),
], );
) },
],
),
); );
} }
} }

View File

@@ -90,8 +90,6 @@ PODS:
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- geolocator_apple (1.2.0):
- FlutterMacOS
- GoogleAppMeasurement (11.8.0): - GoogleAppMeasurement (11.8.0):
- GoogleAppMeasurement/AdIdSupport (= 11.8.0) - GoogleAppMeasurement/AdIdSupport (= 11.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@@ -148,7 +146,7 @@ PODS:
- HotKey - HotKey
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - FlutterMacOS
- livekit_client (2.4.0): - livekit_client (2.4.1):
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@@ -232,7 +230,6 @@ DEPENDENCIES:
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
@@ -307,8 +304,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral :path: Flutter/ephemeral
gal: gal:
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
geolocator_apple:
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
hotkey_manager_macos: hotkey_manager_macos:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
in_app_review: in_app_review:
@@ -372,14 +367,13 @@ SPEC CHECKSUMS:
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
geolocator_apple: 72a78ae3f3e4ec0db62117bd93e34523f5011d58
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: 2e766be2c3ee6274a8e2633b356b98b5eb842987 livekit_client: d03409f83df069a1bb00a4c8dc78c54fb2287262
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5

View File

@@ -314,7 +314,7 @@ packages:
source: hosted source: hosted
version: "0.3.4+2" version: "0.3.4+2"
crypto: crypto:
dependency: transitive dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"

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.4.2+79 version: 2.4.2+81
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -142,6 +142,7 @@ dependencies:
flutter_blurhash: ^0.8.2 flutter_blurhash: ^0.8.2
timelines_plus: ^1.0.6 timelines_plus: ^1.0.6
latlong2: ^0.9.1 latlong2: ^0.9.1
crypto: ^3.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: