Compare commits

..

10 Commits

Author SHA1 Message Date
ae9a7eb0fd 🚀 Launch 2.2.2+61 2025-02-02 13:35:43 +08:00
5d6fb2442f Able to config preferred language 2025-02-01 19:35:50 +08:00
5a85985534 Featured comment 2025-01-31 23:16:14 +08:00
c80499db03 Share to chat channel 2025-01-31 22:52:21 +08:00
b8dcdb2315 💄 Move the connection indicator 2025-01-31 21:50:18 +08:00
b7b921f1f4 📱 Fix new notify indicator on large screen 2025-01-31 20:26:20 +08:00
319d5c7d7f ♻️ Refactor notification indicator 2025-01-31 20:12:46 +08:00
4b5b001739 🐛 Fix open from widget cause multiple activity 2025-01-31 00:39:10 +08:00
db8871a455 🚀 Launch 2.2.2+60 2025-01-31 00:22:06 +08:00
38dcaa6066 AI Post Insight 2025-01-30 14:58:06 +08:00
38 changed files with 948 additions and 156 deletions

View File

@ -26,7 +26,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleInstance"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@ -0,0 +1,26 @@
meta {
name: Developer Notify One User
type: http
seq: 2
}
post {
url: {{endpoint}}/cgi/id/dev/notify/1
body: json
auth: inherit
}
body:json {
{
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "测试",
"subtitle": "Alphabot です",
"content": "全新通知动画",
"metadata": {
"image": "D2EDbcrsTugs3xk5"
},
"priority": 10
}
}

View File

@ -0,0 +1,11 @@
meta {
name: Run Database Maintenance
type: http
seq: 1
}
post {
url: {{endpoint}}/wt/maintenance/database
body: none
auth: inherit
}

View File

@ -1,8 +1,8 @@
vars { vars {
endpoint: https://api.sn.solsynth.dev endpoint: https://api.sn.solsynth.dev
third_client_id: alphabot third_client_id: alphabot
atk: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NvbHN5bnRoLmRldiIsInN1YiI6IjEiLCJhdWQiOlsic29sYXItbmV0d29yayJdLCJleHAiOjE3MzgwODE2OTYsIm5iZiI6MTczODA3ODY5NiwiaWF0IjoxNzM4MDc4Njk2LCJqdGkiOiI1Yzg2MTYxZC00MTZjLTQwNDYtOWFlNS04YWZhNGIyZjdlMTkiLCJzZWQiOiIyMTciLCJ0eXAiOiJhY2Nlc3MifQ.LdLZ6FLb8IqPI__U8sT6VyxK5S_ZgwVGGL-tk01tK9C19wnbFFThPDgga1pJu_VVVpGVxzvMyd-3tBotzLMB5LjhYbtPOJakN2oug1HAgJ8zfc1clORlpHlUVisaiQtl3ZkWtzxni8etZDhJpqHU65IGQG01TO6PZGLgxKkMel4gGeeKhHpg9Q4Eewr3Pbl_wJkHVChJ9IJPmgioc_CACE10nEHZgwptCbndUz3AbIDOG9qW-7ZoprtKoRwAcuUXANK277VvdBRhwQjqKBREqVeMXP_Rv37jDPYhWpfS6HtKoHElZOTJG-69S2Zc3HyHlPJAPlzLAjGPoxScky79Gg
} }
vars:secret [ vars:secret [
atk,
third_client_tk third_client_tk
] ]

View File

@ -241,6 +241,8 @@
"settingsMisc": "Misc", "settingsMisc": "Misc",
"settingsMiscAbout": "About", "settingsMiscAbout": "About",
"settingsMiscAboutDescription": "View the version information of Solian.", "settingsMiscAboutDescription": "View the version information of Solian.",
"settingsAccountLanguage": "Account Language",
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
"sensitiveContent": "Sensitive Content", "sensitiveContent": "Sensitive Content",
"sensitiveContentCollapsed": "Sensitive content has been collapsed.", "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
@ -554,11 +556,15 @@
"postImageShareAds": "Explore posts on the Solar Network", "postImageShareAds": "Explore posts on the Solar Network",
"postShare": "Share", "postShare": "Share",
"postShareImage": "Share via Image", "postShareImage": "Share via Image",
"postGetInsight": "Get Insight",
"postGetInsightTitle": "AI Insight",
"postGetInsightDescription": "AI may make mistakes, check important information.",
"appInitializing": "Initializing", "appInitializing": "Initializing",
"poweredBy": "Powered by {}", "poweredBy": "Powered by {}",
"shareIntent": "Share", "shareIntent": "Share",
"shareIntentDescription": "What do you want to do with the content you are sharing?", "shareIntentDescription": "What do you want to do with the content you are sharing?",
"shareIntentPostStory": "Post a Story", "shareIntentPostStory": "Post a Story",
"shareIntentSendChannel": "Share to Channel",
"updateAvailable": "Update Available", "updateAvailable": "Update Available",
"updateOngoing": "Updating, please wait...", "updateOngoing": "Updating, please wait...",
"custom": "Custom", "custom": "Custom",
@ -571,6 +577,7 @@
"colorSchemeWhite": "White", "colorSchemeWhite": "White",
"colorSchemeBlack": "Black", "colorSchemeBlack": "Black",
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
"postFeaturedComment": "Featured Comment",
"postCategoryTechnology": "Technology", "postCategoryTechnology": "Technology",
"postCategoryGaming": "Gaming", "postCategoryGaming": "Gaming",
"postCategoryLife": "Life", "postCategoryLife": "Life",
@ -600,5 +607,7 @@
"walletCurrency": { "walletCurrency": {
"one": "{} Source Point", "one": "{} Source Point",
"other": "{} Source Points" "other": "{} Source Points"
} },
"aiThinkingProcess": "AI Thinking Process",
"accountSettingsApplied": "Account settings have been applied."
} }

View File

@ -239,6 +239,8 @@
"settingsMisc": "杂项", "settingsMisc": "杂项",
"settingsMiscAbout": "关于", "settingsMiscAbout": "关于",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。", "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帐号偏好语言",
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
"sensitiveContent": "敏感内容", "sensitiveContent": "敏感内容",
"sensitiveContentCollapsed": "敏感内容已折叠。", "sensitiveContentCollapsed": "敏感内容已折叠。",
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
@ -552,11 +554,15 @@
"postImageShareAds": "来 Solar Network 探索更多有趣帖子", "postImageShareAds": "来 Solar Network 探索更多有趣帖子",
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖图", "postShareImage": "分享帖图",
"postGetInsight": "获取见解",
"postGetInsightTitle": "AI 见解",
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想对您分享的内容做些什么?", "shareIntentDescription": "您想对您分享的内容做些什么?",
"shareIntentPostStory": "发布动态", "shareIntentPostStory": "发布动态",
"shareIntentSendChannel": "分享到聊天频道",
"updateAvailable": "检测到更新可用", "updateAvailable": "检测到更新可用",
"updateOngoing": "正在更新,请稍后……", "updateOngoing": "正在更新,请稍后……",
"custom": "自定义", "custom": "自定义",
@ -569,6 +575,7 @@
"colorSchemeWhite": "白色", "colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。", "colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
"postFeaturedComment": "精选评论",
"postCategoryTechnology": "技术", "postCategoryTechnology": "技术",
"postCategoryGaming": "游戏", "postCategoryGaming": "游戏",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@ -598,5 +605,7 @@
"walletCurrency": { "walletCurrency": {
"one": "{} 源点", "one": "{} 源点",
"other": "{} 源点" "other": "{} 源点"
} },
"aiThinkingProcess": "AI 思考过程",
"accountSettingsApplied": "帐号设置已应用。"
} }

View File

@ -239,6 +239,8 @@
"settingsMisc": "雜項", "settingsMisc": "雜項",
"settingsMiscAbout": "關於", "settingsMiscAbout": "關於",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。", "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帳號偏好語言",
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
"sensitiveContent": "敏感內容", "sensitiveContent": "敏感內容",
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
@ -552,11 +554,15 @@
"postImageShareAds": "來 Solar Network 探索更多有趣帖子", "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖圖", "postShareImage": "分享帖圖",
"postGetInsight": "獲取見解",
"postGetInsightTitle": "AI 見解",
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想對您分享的內容做些什麼?", "shareIntentDescription": "您想對您分享的內容做些什麼?",
"shareIntentPostStory": "發佈動態", "shareIntentPostStory": "發佈動態",
"shareIntentSendChannel": "分享到聊天頻道",
"updateAvailable": "檢測到更新可用", "updateAvailable": "檢測到更新可用",
"updateOngoing": "正在更新,請稍後……", "updateOngoing": "正在更新,請稍後……",
"custom": "自定義", "custom": "自定義",
@ -569,6 +575,7 @@
"colorSchemeWhite": "白色", "colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
"postFeaturedComment": "精選評論",
"postCategoryTechnology": "技術", "postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲", "postCategoryGaming": "遊戲",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@ -598,5 +605,7 @@
"walletCurrency": { "walletCurrency": {
"one": "{} 源點", "one": "{} 源點",
"other": "{} 源點" "other": "{} 源點"
} },
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。"
} }

View File

@ -239,6 +239,8 @@
"settingsMisc": "雜項", "settingsMisc": "雜項",
"settingsMiscAbout": "關於", "settingsMiscAbout": "關於",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。", "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帳號偏好語言",
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
"sensitiveContent": "敏感內容", "sensitiveContent": "敏感內容",
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
@ -552,11 +554,15 @@
"postImageShareAds": "來 Solar Network 探索更多有趣帖子", "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖圖", "postShareImage": "分享帖圖",
"postGetInsight": "獲取見解",
"postGetInsightTitle": "AI 見解",
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想對您分享的內容做些什麼?", "shareIntentDescription": "您想對您分享的內容做些什麼?",
"shareIntentPostStory": "發佈動態", "shareIntentPostStory": "發佈動態",
"shareIntentSendChannel": "分享到聊天頻道",
"updateAvailable": "檢測到更新可用", "updateAvailable": "檢測到更新可用",
"updateOngoing": "正在更新,請稍後……", "updateOngoing": "正在更新,請稍後……",
"custom": "自定義", "custom": "自定義",
@ -569,6 +575,7 @@
"colorSchemeWhite": "白色", "colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
"postFeaturedComment": "精選評論",
"postCategoryTechnology": "技術", "postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲", "postCategoryGaming": "遊戲",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@ -598,5 +605,7 @@
"walletCurrency": { "walletCurrency": {
"one": "{} 源點", "one": "{} 源點",
"other": "{} 源點" "other": "{} 源點"
} },
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。"
} }

View File

@ -379,7 +379,7 @@ SPEC CHECKSUMS:
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e

View File

@ -71,22 +71,29 @@ class NotificationProvider extends ChangeNotifier {
); );
} }
int showingCount = 0;
List<SnNotification> notifications = List.empty(growable: true); List<SnNotification> notifications = List.empty(growable: true);
void listen() { void listen() {
_ws.stream.stream.listen((event) { _ws.stream.stream.listen((event) {
if (event.method == 'notifications.new') { if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!); final notification = SnNotification.fromJson(event.payload!);
if (showingCount < 0) showingCount = 0;
showingCount++;
notifications.add(notification); notifications.add(notification);
Future.delayed(const Duration(seconds: 3), () {
if (showingCount >= 0) showingCount--;
notifyListeners();
});
notifyListeners(); notifyListeners();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.lightImpact(); if (doHaptic) HapticFeedback.mediumImpact();
} }
}); });
} }
void clear() { void clear() {
notifications.clear(); showingCount = 0;
notifyListeners(); notifyListeners();
} }
} }

View File

@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
user = null; user = null;
notifyListeners(); notifyListeners();
} }
void setLanguage(String? value) {
if (value == null) return;
if (user == null) return;
user = user!.copyWith(language: value);
notifyListeners();
}
} }

View File

@ -73,7 +73,7 @@ final _appRoutes = [
postRepostId: int.tryParse( postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '', state.uri.queryParameters['reposting'] ?? '',
), ),
extraProps: state.extra as PostEditorExtraProps?, extraProps: state.extra as PostEditorExtra?,
), ),
), ),
GoRoute( GoRoute(
@ -156,6 +156,7 @@ final _appRoutes = [
builder: (context, state) => ChatRoomScreen( builder: (context, state) => ChatRoomScreen(
scope: state.pathParameters['scope']!, scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!, alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?,
), ),
), ),
GoRoute( GoRoute(

View File

@ -28,7 +28,19 @@ class AccountScreen extends StatelessWidget {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text(
"screenAccount",
style: TextStyle(
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(255, 0, 0, 0),
),
],
),
).tr(),
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
? Stack( ? Stack(
fit: StackFit.expand, fit: StackFit.expand,

View File

@ -1,17 +1,41 @@
import 'package:collection/collection.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
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:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:intl/locale.dart';
class AccountSettingsScreen extends StatelessWidget { class AccountSettingsScreen extends StatelessWidget {
const AccountSettingsScreen({super.key}); const AccountSettingsScreen({super.key});
Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
if (value == null) return;
try {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
await sn.client.put('/cgi/id/users/me/language', data: {
'language': value.toString(),
});
if (!context.mounted) return;
context.showSnackbar('accountSettingsApplied'.tr());
await ua.refreshUser();
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
@ -21,6 +45,42 @@ class AccountSettingsScreen extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile(
title: Text('settingsAccountLanguage').tr(),
subtitle: Text('settingsAccountLanguageDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<Locale?>(
isExpanded: true,
items: [
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
return DropdownMenuItem<Locale?>(
value: Locale.parse(ele.toString()),
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
);
}),
],
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
onChanged: (Locale? value) {
if (value == null) return;
_setAccountLanguage(context, value);
ua.setLanguage(value.toString());
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
height: 40,
width: 160,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
),
),
ListTile( ListTile(
title: Text('accountProfileEdit').tr(), title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(), subtitle: Text('accountProfileEditSubtitle').tr(),

View File

@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
'nick': nickname, 'nick': nickname,
'email': email, 'email': email,
'password': password, 'password': password,
'language': EasyLocalization.of(context)!.currentLocale.toString(),
}); });
if (!context.mounted) return; if (!context.mounted) return;

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart'; import 'package:surface/widgets/chat/call/call_prejoin.dart';
@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.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/user_directory.dart'; class ChatRoomScreenExtra {
import '../../providers/userinfo.dart'; final String? initialText;
final List<PostWriteMedia>? initialAttachments;
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
}
class ChatRoomScreen extends StatefulWidget { class ChatRoomScreen extends StatefulWidget {
final String scope; final String scope;
final String alias; final String alias;
final ChatRoomScreenExtra? extra;
const ChatRoomScreen({super.key, required this.scope, required this.alias}); const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
@override @override
State<ChatRoomScreen> createState() => _ChatRoomScreenState(); State<ChatRoomScreen> createState() => _ChatRoomScreenState();
@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
_messageController = ChatMessageController(context); _messageController = ChatMessageController(context);
_fetchChannel().then((_) async { _fetchChannel().then((_) async {
await _messageController.initialize(_channel!); await _messageController.initialize(_channel!);
await _messageController.checkUpdate();
await _fetchOngoingCall(); if (widget.extra != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
log('[ChatInput] Setting initial text and attachments...');
if (widget.extra!.initialText != null) {
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
}
if (widget.extra!.initialAttachments != null) {
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
}
});
}
await Future.wait([
_messageController.checkUpdate(),
_fetchOngoingCall(),
]);
}); });
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();

View File

@ -288,6 +288,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
child: InkWell( child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4, spacing: 4,
children: [ children: [
Text( Text(

View File

@ -21,6 +21,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart'; import '../widgets/unauthorized_hint.dart';
const Map<String, IconData> kNotificationTopicIcons = {
'general': Symbols.notifications,
'passport.security.alert': Symbols.gpp_maybe,
'passport.security.otp': Symbols.password,
'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction,
'messaging.callStart': Symbols.call_received,
'wallet.transaction.new': Symbols.receipt,
};
class NotificationScreen extends StatefulWidget { class NotificationScreen extends StatefulWidget {
const NotificationScreen({super.key}); const NotificationScreen({super.key});
@ -36,15 +46,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
final List<SnNotification> _notifications = List.empty(growable: true); final List<SnNotification> _notifications = List.empty(growable: true);
int? _totalCount; int? _totalCount;
static const Map<String, IconData> kNotificationTopicIcons = {
'passport.security.alert': Symbols.gpp_maybe,
'passport.security.otp': Symbols.password,
'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction,
'messaging.callStart': Symbols.call_received,
'wallet.transaction.new': Symbols.receipt,
};
Future<void> _fetchNotifications() async { Future<void> _fetchNotifications() async {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) return; if (!ua.isAuthorized) return;

View File

@ -20,13 +20,13 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class PostEditorExtraProps { class PostEditorExtra {
final String? text; final String? text;
final String? title; final String? title;
final String? description; final String? description;
final List<PostWriteMedia>? attachments; final List<PostWriteMedia>? attachments;
const PostEditorExtraProps({ const PostEditorExtra({
this.text, this.text,
this.title, this.title,
this.description, this.description,
@ -39,7 +39,7 @@ class PostEditorScreen extends StatefulWidget {
final int? postEditId; final int? postEditId;
final int? postReplyId; final int? postReplyId;
final int? postRepostId; final int? postRepostId;
final PostEditorExtraProps? extraProps; final PostEditorExtra? extraProps;
const PostEditorScreen({ const PostEditorScreen({
super.key, super.key,

View File

@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/screens/post/post_editor.dart'; import 'package:surface/screens/post/post_editor.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
class AppSharingListener extends StatefulWidget { class AppSharingListener extends StatefulWidget {
final Widget child; final Widget child;
@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
pathParameters: { pathParameters: {
'mode': 'stories', 'mode': 'stories',
}, },
extra: PostEditorExtraProps( extra: PostEditorExtra(
text: value text: value
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path).join('\n'), .map((e) => e.path)
.join('\n'),
attachments: value attachments: value
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(), .contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
), ),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
leading: Icon(Icons.chat_outlined),
trailing: const Icon(Icons.chevron_right),
title: Text('shareIntentSendChannel').tr(),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _ShareIntentChannelSelect(value: value),
).then((val) {
if (!context.mounted) return;
if (val == true) Navigator.pop(context);
});
},
),
], ],
), ).width(280),
) )
], ],
), ),
@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
_initialize(); _initialize();
_initialHandle(); _initialHandle();
} }
@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
return widget.child; return widget.child;
} }
} }
class _ShareIntentChannelSelect extends StatefulWidget {
final Iterable<SharedMediaFile> value;
const _ShareIntentChannelSelect({required this.value});
@override
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
}
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
bool _isBusy = true;
List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages;
void _refreshChannels() {
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) {
setState(() => _isBusy = false);
return;
}
final chan = context.read<ChatChannelProvider>();
chan.fetchChannels().listen((channels) async {
final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) {
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
}
if (_lastMessages!.containsKey(a.id)) return -1;
if (_lastMessages!.containsKey(b.id)) return 1;
return 0;
});
if (!mounted) return;
final ud = context.read<UserDirectoryProvider>();
for (final channel in channels) {
if (channel.type == 1) {
await ud.listAccount(
channel.members
?.cast<SnChannelMember?>()
.map((ele) => ele?.accountId)
.where((ele) => ele != null)
.toSet() ??
{},
);
}
}
if (mounted) setState(() => _channels = channels);
})
..onError((err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
})
..onDone(() {
if (!mounted) return;
setState(() => _isBusy = false);
});
}
@override
void initState() {
super.initState();
_refreshChannels();
}
@override
Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final ud = context.read<UserDirectoryProvider>();
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.chat, size: 24),
const Gap(16),
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
LoadingIndicator(isActive: _isBusy),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _refreshChannels()),
child: ListView.builder(
itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels();
});
},
);
}
return ListTile(
title: Text(channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onTap: () {
Navigator.pop(context, true);
GoRouter.of(context)
.pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
extra: ChatRoomScreenExtra(
initialText: widget.value
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path)
.join('\n'),
initialAttachments: widget.value
.where((e) =>
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
),
)
.then((value) {
if (value == true) _refreshChannels();
});
},
);
},
),
),
),
),
],
);
}
}

View File

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:developer';
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:gap/gap.dart';

View File

@ -21,6 +21,7 @@ class SnAccount with _$SnAccount {
required String name, required String name,
required String nick, required String nick,
required Map<String, dynamic> permNodes, required Map<String, dynamic> permNodes,
required String language,
required SnAccountProfile? profile, required SnAccountProfile? profile,
@Default([]) List<SnAccountBadge> badges, @Default([]) List<SnAccountBadge> badges,
required DateTime? suspendedAt, required DateTime? suspendedAt,

View File

@ -33,6 +33,7 @@ mixin _$SnAccount {
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
String get nick => throw _privateConstructorUsedError; String get nick => throw _privateConstructorUsedError;
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError; Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
String get language => throw _privateConstructorUsedError;
SnAccountProfile? get profile => throw _privateConstructorUsedError; SnAccountProfile? get profile => throw _privateConstructorUsedError;
List<SnAccountBadge> get badges => throw _privateConstructorUsedError; List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
DateTime? get suspendedAt => throw _privateConstructorUsedError; DateTime? get suspendedAt => throw _privateConstructorUsedError;
@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
String name, String name,
String nick, String nick,
Map<String, dynamic> permNodes, Map<String, dynamic> permNodes,
String language,
SnAccountProfile? profile, SnAccountProfile? profile,
List<SnAccountBadge> badges, List<SnAccountBadge> badges,
DateTime? suspendedAt, DateTime? suspendedAt,
@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
Object? name = null, Object? name = null,
Object? nick = null, Object? nick = null,
Object? permNodes = null, Object? permNodes = null,
Object? language = null,
Object? profile = freezed, Object? profile = freezed,
Object? badges = null, Object? badges = null,
Object? suspendedAt = freezed, Object? suspendedAt = freezed,
@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
? _value.permNodes ? _value.permNodes
: permNodes // ignore: cast_nullable_to_non_nullable : permNodes // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>, as Map<String, dynamic>,
language: null == language
? _value.language
: language // ignore: cast_nullable_to_non_nullable
as String,
profile: freezed == profile profile: freezed == profile
? _value.profile ? _value.profile
: profile // ignore: cast_nullable_to_non_nullable : profile // ignore: cast_nullable_to_non_nullable
@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
String name, String name,
String nick, String nick,
Map<String, dynamic> permNodes, Map<String, dynamic> permNodes,
String language,
SnAccountProfile? profile, SnAccountProfile? profile,
List<SnAccountBadge> badges, List<SnAccountBadge> badges,
DateTime? suspendedAt, DateTime? suspendedAt,
@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
Object? name = null, Object? name = null,
Object? nick = null, Object? nick = null,
Object? permNodes = null, Object? permNodes = null,
Object? language = null,
Object? profile = freezed, Object? profile = freezed,
Object? badges = null, Object? badges = null,
Object? suspendedAt = freezed, Object? suspendedAt = freezed,
@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
? _value._permNodes ? _value._permNodes
: permNodes // ignore: cast_nullable_to_non_nullable : permNodes // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>, as Map<String, dynamic>,
language: null == language
? _value.language
: language // ignore: cast_nullable_to_non_nullable
as String,
profile: freezed == profile profile: freezed == profile
? _value.profile ? _value.profile
: profile // ignore: cast_nullable_to_non_nullable : profile // ignore: cast_nullable_to_non_nullable
@ -373,6 +386,7 @@ class _$SnAccountImpl extends _SnAccount {
required this.name, required this.name,
required this.nick, required this.nick,
required final Map<String, dynamic> permNodes, required final Map<String, dynamic> permNodes,
required this.language,
required this.profile, required this.profile,
final List<SnAccountBadge> badges = const [], final List<SnAccountBadge> badges = const [],
required this.suspendedAt, required this.suspendedAt,
@ -429,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
return EqualUnmodifiableMapView(_permNodes); return EqualUnmodifiableMapView(_permNodes);
} }
@override
final String language;
@override @override
final SnAccountProfile? profile; final SnAccountProfile? profile;
final List<SnAccountBadge> _badges; final List<SnAccountBadge> _badges;
@ -453,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
@override @override
String toString() { String toString() {
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
} }
@override @override
@ -479,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
(identical(other.nick, nick) || other.nick == nick) && (identical(other.nick, nick) || other.nick == nick) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._permNodes, _permNodes) && .equals(other._permNodes, _permNodes) &&
(identical(other.language, language) ||
other.language == language) &&
(identical(other.profile, profile) || other.profile == profile) && (identical(other.profile, profile) || other.profile == profile) &&
const DeepCollectionEquality().equals(other._badges, _badges) && const DeepCollectionEquality().equals(other._badges, _badges) &&
(identical(other.suspendedAt, suspendedAt) || (identical(other.suspendedAt, suspendedAt) ||
@ -509,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
name, name,
nick, nick,
const DeepCollectionEquality().hash(_permNodes), const DeepCollectionEquality().hash(_permNodes),
language,
profile, profile,
const DeepCollectionEquality().hash(_badges), const DeepCollectionEquality().hash(_badges),
suspendedAt, suspendedAt,
@ -548,6 +567,7 @@ abstract class _SnAccount extends SnAccount {
required final String name, required final String name,
required final String nick, required final String nick,
required final Map<String, dynamic> permNodes, required final Map<String, dynamic> permNodes,
required final String language,
required final SnAccountProfile? profile, required final SnAccountProfile? profile,
final List<SnAccountBadge> badges, final List<SnAccountBadge> badges,
required final DateTime? suspendedAt, required final DateTime? suspendedAt,
@ -586,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
@override @override
Map<String, dynamic> get permNodes; Map<String, dynamic> get permNodes;
@override @override
String get language;
@override
SnAccountProfile? get profile; SnAccountProfile? get profile;
@override @override
List<SnAccountBadge> get badges; List<SnAccountBadge> get badges;

View File

@ -26,6 +26,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
name: json['name'] as String, name: json['name'] as String,
nick: json['nick'] as String, nick: json['nick'] as String,
permNodes: json['perm_nodes'] as Map<String, dynamic>, permNodes: json['perm_nodes'] as Map<String, dynamic>,
language: json['language'] as String,
profile: json['profile'] == null profile: json['profile'] == null
? null ? null
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), : SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
'name': instance.name, 'name': instance.name,
'nick': instance.nick, 'nick': instance.nick,
'perm_nodes': instance.permNodes, 'perm_nodes': instance.permNodes,
'language': instance.language,
'profile': instance.profile?.toJson(), 'profile': instance.profile?.toJson(),
'badges': instance.badges.map((e) => e.toJson()).toList(), 'badges': instance.badges.map((e) => e.toJson()).toList(),
'suspended_at': instance.suspendedAt?.toIso8601String(), 'suspended_at': instance.suspendedAt?.toIso8601String(),

View File

@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Scaffold( child: Scaffold(
backgroundColor: Colors.transparent,
body: Stack( body: Stack(
children: [ children: [
Builder(builder: (context) { Builder(builder: (context) {

View File

@ -46,6 +46,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
setState(() => _replyingMessage = value); setState(() => _replyingMessage = value);
} }
void setInitialText(String? value) {
_contentController.text = value ?? '';
setState(() {});
}
void setInitialAttachments(List<PostWriteMedia>? value) {
_attachments.addAll(value ?? []);
setState(() {});
}
void setEdit(SnChatMessage? value) { void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? ''; _contentController.text = value?.body['text'] ?? '';
_attachments.clear(); _attachments.clear();

View File

@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.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';
@ -13,6 +14,9 @@ class ConnectionIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ws = context.watch<WebSocketProvider>(); final ws = context.watch<WebSocketProvider>();
final cfg = context.watch<ConfigProvider>();
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
return ListenableBuilder( return ListenableBuilder(
listenable: ws, listenable: ws,
@ -22,45 +26,50 @@ class ConnectionIndicator extends StatelessWidget {
return IgnorePointer( return IgnorePointer(
ignoring: !show, ignoring: !show,
child: GestureDetector( child: Center(
child: Material( child: GestureDetector(
elevation: 2, child: Material(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), elevation: 2,
color: Theme.of(context).colorScheme.secondaryContainer, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
child: ua.isAuthorized color: Theme.of(context).colorScheme.secondaryContainer,
? Row( child: ua.isAuthorized
mainAxisAlignment: MainAxisAlignment.center, ? Row(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ mainAxisAlignment: MainAxisAlignment.center,
if (ws.isBusy) crossAxisAlignment: CrossAxisAlignment.center,
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) children: [
else if (!ws.isConnected) if (ws.isBusy)
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
else else if (!ws.isConnected)
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), Text('serverDisconnected')
const Gap(8), .tr()
if (ws.isBusy) .textColor(Theme.of(context).colorScheme.onSecondaryContainer)
const CircularProgressIndicator(strokeWidth: 2.5) else
.width(12) Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
.height(12) const Gap(8),
.padding(horizontal: 4, right: 4) if (ws.isBusy)
else if (!ws.isConnected) const CircularProgressIndicator(strokeWidth: 2.5)
const Icon(Symbols.power_off, size: 18) .width(12)
else .height(12)
const Icon(Symbols.power, size: 18), .padding(horizontal: 4, right: 4)
], else if (!ws.isConnected)
).padding(horizontal: 8, vertical: 4) const Icon(Symbols.power_off, size: 18)
: const SizedBox.shrink(), else
).opacity(show ? 1 : 0, animate: true).animate( const Icon(Symbols.power, size: 18),
const Duration(milliseconds: 300), ],
Curves.easeInOut, ).padding(horizontal: 8, vertical: 4)
), : const SizedBox.shrink(),
onTap: () { ).opacity(show ? 1 : 0, animate: true).animate(
if (!ws.isConnected && !ws.isBusy) { const Duration(milliseconds: 300),
ws.connect(); Curves.easeInOut,
} ),
}, onTap: () {
), if (!ws.isConnected && !ws.isBusy) {
ws.connect();
}
},
),
).padding(left: marginLeft),
); );
}, },
); );

View File

@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget {
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = cfg.drawerIsExpanded mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2); : mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
} }
}, },
child: GestureDetector( child: GestureDetector(

View File

@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
final bool isAutoWarp; final bool isAutoWarp;
final bool isEnlargeSticker; final bool isEnlargeSticker;
final TextScaler? textScaler; final TextScaler? textScaler;
final Color? textColor;
final List<SnAttachment?>? attachments; final List<SnAttachment?>? attachments;
const MarkdownTextContent({ const MarkdownTextContent({
@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
this.isAutoWarp = false, this.isAutoWarp = false,
this.isEnlargeSticker = false, this.isEnlargeSticker = false,
this.textScaler, this.textScaler,
this.textColor,
this.attachments, this.attachments,
}); });
@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaler: textScaler, textScaler: textScaler,
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
blockquote: TextStyle( blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),

View File

@ -31,34 +31,37 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
builder: (context, _) { builder: (context, _) {
final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
return NavigationRail( return SizedBox(
selectedIndex: width: 80,
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, child: NavigationRail(
destinations: [ selectedIndex:
...destinations.where((ele) => ele.isPinned).map((ele) { nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
return NavigationRailDestination( destinations: [
icon: ele.icon, ...destinations.where((ele) => ele.isPinned).map((ele) {
label: Text(ele.label).tr(), return NavigationRailDestination(
); icon: ele.icon,
}), label: Text(ele.label).tr(),
], );
trailing: Expanded( }),
child: Align( ],
alignment: Alignment.bottomCenter, trailing: Expanded(
child: StyledWidget( child: Align(
IconButton( alignment: Alignment.bottomCenter,
icon: const Icon(Symbols.menu), child: StyledWidget(
onPressed: () { IconButton(
Scaffold.of(context).openDrawer(); icon: const Icon(Symbols.menu),
}, onPressed: () {
), Scaffold.of(context).openDrawer();
).padding(bottom: 16), },
),
).padding(bottom: 16),
),
), ),
onDestinationSelected: (idx) {
nav.setIndex(idx);
GoRouter.of(context).goNamed(destinations[idx].screen);
},
), ),
onDestinationSelected: (idx) {
nav.setIndex(idx);
GoRouter.of(context).goNamed(destinations[idx].screen);
},
); );
}, },
); );

View File

@ -140,6 +140,7 @@ class AppRootScaffold extends StatelessWidget {
); );
final safeTop = MediaQuery.of(context).padding.top; final safeTop = MediaQuery.of(context).padding.top;
final safeBottom = MediaQuery.of(context).padding.bottom;
return Scaffold( return Scaffold(
key: globalRootScaffoldKey, key: globalRootScaffoldKey,
@ -191,7 +192,10 @@ class AppRootScaffold extends StatelessWidget {
], ],
), ),
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
else
Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
], ],
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,

View File

@ -1,60 +1,181 @@
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:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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/notification.dart'; import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/notification.dart';
import 'package:surface/types/notification.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart';
class NotifyIndicator extends StatelessWidget { import 'markdown_content.dart';
class NotifyIndicator extends StatefulWidget {
const NotifyIndicator({super.key}); const NotifyIndicator({super.key});
@override
State<NotifyIndicator> createState() => _NotifyIndicatorState();
}
class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin {
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
void _markOneAsRead(SnNotification notification) async {
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) return;
if (notification.id == 0) return;
if (notification.readAt != null) return;
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/id/notifications/read/${notification.id}');
if (!mounted) return;
context.showSnackbar(
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final nty = context.watch<NotificationProvider>(); final nty = context.watch<NotificationProvider>();
final show = nty.notifications.isNotEmpty && ua.isAuthorized; final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
final show = nty.showingCount > 0 && ua.isAuthorized;
if (show) {
_animationController.animateTo(1);
} else {
_animationController.animateTo(0);
}
return ListenableBuilder( return ListenableBuilder(
listenable: nty, listenable: nty,
builder: (context, _) { builder: (context, _) {
final current = nty.notifications.lastOrNull;
return IgnorePointer( return IgnorePointer(
ignoring: !show, ignoring: !show,
child: GestureDetector( child: GestureDetector(
child: Material( child: Animate(
elevation: 2, autoPlay: false,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), controller: _animationController,
color: Theme.of(context).colorScheme.secondaryContainer, effects: [
child: ua.isAuthorized SlideEffect(
? Row( begin: isMobile ? Offset(0, -1) : Offset(1, 0),
mainAxisAlignment: MainAxisAlignment.center, end: Offset(0, 0),
crossAxisAlignment: CrossAxisAlignment.center, duration: Duration(milliseconds: 300),
children: [ curve: Curves.fastEaseInToSlowEaseOut,
Text(
nty.notifications.lastOrNull?.title ??
'notificationUnreadCount'.plural(nty.notifications.length),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (nty.notifications.lastOrNull?.body != null)
Text(
nty.notifications.lastOrNull!.body,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).padding(left: 4),
const Gap(8),
const Icon(Symbols.notifications_unread, size: 18),
],
).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(),
).opacity(show ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300),
Curves.easeInOut,
), ),
FadeEffect(
begin: 0.0,
end: 1.0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
),
],
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
width: double.infinity,
constraints: BoxConstraints(
maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360,
),
child: Material(
elevation: 2,
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surfaceContainer,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (current?.metadata['avatar'] != null)
CircleAvatar(
radius: 14,
backgroundImage: UniversalImage.provider(
sn.getAttachmentUrl(current!.metadata['avatar']),
),
)
else
Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications),
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
current?.title ?? 'Notification',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
),
),
if (current?.subtitle?.isNotEmpty ?? false)
Text(
current!.subtitle!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
),
),
MarkdownTextContent(
content: current?.body ?? '',
isAutoWarp: true,
),
],
),
),
const Gap(16),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? DateTime.now()))
.fontSize(12)
.padding(right: 2),
const Gap(6),
if (current?.metadata['image'] != null)
SizedBox(
width: 40,
height: 40,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(current?.metadata['image']),
fit: BoxFit.cover,
),
),
),
],
),
],
).padding(horizontal: 16, vertical: 12),
),
),
),
onTap: () { onTap: () {
nty.clear(); nty.clear();
if (current != null) {
_markOneAsRead(current);
}
}, },
), ),
); );

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -34,6 +36,7 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/post/post_reaction.dart'; import 'package:surface/widgets/post/post_reaction.dart';
import 'package:surface/widgets/post/publisher_popover.dart'; import 'package:surface/widgets/post/publisher_popover.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:xml/xml.dart';
class PostItem extends StatelessWidget { class PostItem extends StatelessWidget {
final SnPost data; final SnPost data;
@ -186,6 +189,7 @@ class PostItem extends StatelessWidget {
), ),
), ),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
_PostBottomAction( _PostBottomAction(
data: data, data: data,
showComments: showComments, showComments: showComments,
@ -267,6 +271,7 @@ class PostItem extends StatelessWidget {
LinkPreviewWidget( LinkPreviewWidget(
text: data.body['content'], text: data.body['content'],
).padding(horizontal: 4), ).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
Container( Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column( child: Column(
@ -817,6 +822,22 @@ class _PostContentHeader extends StatelessWidget {
}, },
), ),
const PopupMenuDivider(), const PopupMenuDivider(),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.book_4_spark),
const Gap(16),
Text('postGetInsight').tr(),
],
),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _PostGetInsightSheet(postId: data.id),
);
},
),
const PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
onTap: onShare, onTap: onShare,
child: Row( child: Row(
@ -1106,6 +1127,95 @@ class _PostTruncatedHint extends StatelessWidget {
} }
} }
class _PostFeaturedComment extends StatefulWidget {
final SnPost data;
final double? maxWidth;
const _PostFeaturedComment({required this.data, this.maxWidth});
@override
State<_PostFeaturedComment> createState() => _PostFeaturedCommentState();
}
class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
SnPost? _featuredComment;
Future<void> _fetchComments() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
'take': 1,
});
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
@override
void initState() {
super.initState();
if (widget.data.metric.replyCount > 0) {
_fetchComments();
}
}
@override
Widget build(BuildContext context) {
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
if (_featuredComment == null) return const SizedBox.shrink();
return AnimateWidgetExtensions(Container(
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
margin: const EdgeInsets.only(top: 8),
width: double.infinity,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (context) => PostCommentListPopup(
postId: widget.data.id,
commentCount: widget.data.metric.replyCount,
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(),
const Gap(4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(
radius: 12,
backgroundImage: UniversalImage.provider(
_featuredComment!.publisher.avatar,
),
),
const Gap(8),
Expanded(
child: MarkdownTextContent(
content: _featuredComment!.body['content'],
isAutoWarp: true,
),
)
],
),
],
).padding(horizontal: 16, vertical: 8),
),
),
)).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut);
}
}
class _PostAbuseReportDialog extends StatefulWidget { class _PostAbuseReportDialog extends StatefulWidget {
final SnPost data; final SnPost data;
@ -1181,3 +1291,96 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
); );
} }
} }
class _PostGetInsightSheet extends StatefulWidget {
final int postId;
const _PostGetInsightSheet({required this.postId});
@override
State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState();
}
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
String? _response;
String? _thinkingProcess;
Future<void> _fetchResponse() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.postId}/insight',
options: Options(
sendTimeout: const Duration(minutes: 10),
receiveTimeout: const Duration(minutes: 10),
));
final out = resp.data['response'] as String;
final document = XmlDocument.parse(out);
_thinkingProcess = document.getElement('think')?.innerText.trim();
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
@override
void initState() {
super.initState();
_fetchResponse();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.book_4_spark, size: 24),
const Gap(16),
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
const Gap(4),
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
const Gap(4),
if (_response == null)
Expanded(
child: Center(
child: CircularProgressIndicator(),
),
)
else
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
if (_thinkingProcess != null && _thinkingProcess!.isNotEmpty)
ExpansionTile(
leading: const Icon(Symbols.info),
title: Text('aiThinkingProcess'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
minTileHeight: 32,
children: [
SelectableText(
_thinkingProcess!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
).padding(horizontal: 20, vertical: 8),
],
).padding(vertical: 8),
SelectionArea(
child: MarkdownTextContent(
content: _response!,
),
).padding(horizontal: 20, top: 8),
],
),
),
),
],
);
}
}

View File

@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
); );
} }
} }
HapticFeedback.mediumImpact(); HapticFeedback.heavyImpact();
} catch (err) { } catch (err) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);

View File

@ -8,6 +8,7 @@ import Foundation
import bitsdojo_window_macos import bitsdojo_window_macos
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
import file_picker
import file_saver import file_saver
import file_selector_macos import file_selector_macos
import firebase_analytics import firebase_analytics
@ -36,6 +37,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))

View File

@ -8,6 +8,8 @@ PODS:
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_saver (0.0.1): - file_saver (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1): - file_selector_macos (0.0.1):
@ -185,6 +187,7 @@ DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`) - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
@ -237,6 +240,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
device_info_plus: device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_saver: file_saver:
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
file_selector_macos: file_selector_macos:
@ -293,6 +298,7 @@ SPEC CHECKSUMS:
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
croppy: 25a638bd7d05411d8c697f481568f261037694fc croppy: 25a638bd7d05411d8c697f481568f261037694fc
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b Firebase: 374a441a91ead896215703a674d58cdb3e9d772b

View File

@ -362,10 +362,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544 sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.2.1" version: "11.2.2"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -378,10 +378,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.7.0" version: "5.8.0+1"
dio_smart_retry: dio_smart_retry:
dependency: "direct main" dependency: "direct main"
description: description:
@ -394,10 +394,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dio_web_adapter name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.1.0"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@ -490,10 +490,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204 sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.7" version: "8.3.1"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@ -886,10 +886,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: daf3ff5570f55396b2d2c9bf8136d7db3a8acf208ac0cef92a3ae2beb9a81550 sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.7.1" version: "14.7.2"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1334,10 +1334,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.3" version: "8.1.4"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1710,10 +1710,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.5.1"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
@ -2059,10 +2059,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics name: vector_graphics
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.15" version: "1.1.16"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
@ -2216,7 +2216,7 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml: xml:
dependency: transitive dependency: "direct main"
description: description:
name: xml name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226

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.2.2+59 version: 2.2.2+61
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -117,6 +117,7 @@ dependencies:
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
flutter_inappwebview: ^6.1.5 flutter_inappwebview: ^6.1.5
html: ^0.15.5 html: ^0.15.5
xml: ^6.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: