Compare commits
12 Commits
ac2aec48aa
...
2.4.2+81
| Author | SHA1 | Date | |
|---|---|---|---|
| d6013078bd | |||
| 5976d61997 | |||
| b492db90ca | |||
| c9f69fed2c | |||
| d2f4e7a969 | |||
| aecd04e0b9 | |||
| e5212419ae | |||
| ec7650a920 | |||
| 7b96013406 | |||
| fc5a79b29b | |||
| 4146820be5 | |||
| 9ec0f1ff19 |
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "在查看帖子、消息时自动翻译文本。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "在查看帖子、消息時自動翻譯文本。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "在查看帖子、消息時自動翻譯文本。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
56
lib/providers/translation.dart
Normal file
56
lib/providers/translation.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/screens/account/prefs/notify.dart
Normal file
11
lib/screens/account/prefs/notify.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,12 +45,26 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
case 'image':
|
case 'image':
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${data!.rid}-$tag',
|
tag: 'attachment-${data!.rid}-$tag',
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
ImageFiltered(
|
||||||
|
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(data!.rid),
|
||||||
|
key: Key('attachment-${data!.rid}-$tag-blur-background'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data!.rid),
|
sn.getAttachmentUrl(data!.rid),
|
||||||
key: Key('attachment-${data!.rid}-$tag'),
|
key: Key('attachment-${data!.rid}-$tag'),
|
||||||
fit: fit,
|
fit: fit,
|
||||||
filterQuality: filterQuality,
|
filterQuality: filterQuality,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
return _AttachmentItemContentVideo(
|
return _AttachmentItemContentVideo(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ 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(
|
||||||
@@ -101,7 +100,6 @@ class NewsFeedEntry extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ 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),
|
||||||
@@ -21,7 +20,6 @@ class FeedUnknownEntry extends StatelessWidget {
|
|||||||
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);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ class FediversePostWidget extends StatelessWidget {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -75,7 +73,6 @@ class FediversePostWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +189,9 @@ 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(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -188,7 +199,9 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.comment, size: 24),
|
const Icon(Symbols.comment, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
|
Text('postCommentsDetailed')
|
||||||
|
.plural(widget.commentCount)
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -197,6 +210,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
|||||||
if (ua.isAuthorized)
|
if (ua.isAuthorized)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
height: 240,
|
height: 240,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
@@ -222,6 +236,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,8 @@ class _PostPollState extends State<PostPoll> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -91,10 +93,11 @@ class _PostPollState extends State<PostPoll> {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 60,
|
height: 60,
|
||||||
width: MediaQuery.of(context).size.width *
|
width: constraints.maxWidth *
|
||||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||||
.toDouble(),
|
.toDouble(),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -114,8 +117,8 @@ class _PostPollState extends State<PostPoll> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'pollVotes'
|
'pollVotes'.plural(
|
||||||
.plural(_poll.metric.byOptions[option.id] ?? 0),
|
_poll.metric.byOptions[option.id] ?? 0),
|
||||||
),
|
),
|
||||||
Text(' · ').padding(horizontal: 4),
|
Text(' · ').padding(horizontal: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -134,5 +137,7 @@ class _PostPollState extends State<PostPoll> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user