Compare commits

...

18 Commits

Author SHA1 Message Date
52f1826e91 🚀 Launch 2.2.2+62 2025-02-04 22:56:45 +08:00
28a4c86dbf Optimize post editor 2025-02-04 22:04:50 +08:00
85e48ce03b ♻️ Refactor tray with manager 2025-02-04 16:11:25 +08:00
efef61a8ea Tray icon basis 2025-02-04 15:43:20 +08:00
10ead95af9 Emotes picker 2025-02-04 02:33:19 +08:00
838ee4d55d Click to zoom in sticker 2025-02-03 22:56:49 +08:00
13e42429a9 📝 Update API docs 2025-02-03 21:34:15 +08:00
c6ce3fe2b7 🐛 Patch websocket connection issue 2025-02-03 21:34:05 +08:00
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
55 changed files with 1540 additions and 282 deletions

View File

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

View File

@ -12,9 +12,9 @@ post {
body:json {
{
"alias": "AteChip",
"name": "Cat ate chips",
"attachment_id": "d0b692cc64054463",
"pack_id": 2
"alias": "Meltdown",
"name": "Meltdown",
"attachment_id": "IpDPHEbWDDCbBofX",
"pack_id": 4
}
}

View File

@ -0,0 +1,11 @@
meta {
name: Get Sticker Packs
type: http
seq: 3
}
get {
url: {{endpoint}}/cgi/uc/stickers/packs
body: none
auth: none
}

View File

@ -0,0 +1,15 @@
meta {
name: Get Stickers
type: http
seq: 4
}
get {
url: {{endpoint}}/cgi/uc/stickers?take=10
body: none
auth: none
}
params:query {
take: 10
}

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,20 @@
meta {
name: Create Order
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/wa/orders
body: json
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -0,0 +1,21 @@
meta {
name: Create Transaction
type: http
seq: 3
}
post {
url: {{endpoint}}/cgi/wa/transactions
body: json
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "秦始皇的赞赏",
"amount": 500,
"payee_id": 1
}
}

20
api/Wallet/Get Order.bru Normal file
View File

@ -0,0 +1,20 @@
meta {
name: Get Order
type: http
seq: 2
}
get {
url: {{endpoint}}/cgi/wa/orders/4
body: none
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -0,0 +1,20 @@
meta {
name: Get Transaction
type: http
seq: 4
}
get {
url: {{endpoint}}/cgi/wa/transactions/67
body: none
auth: inherit
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

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 {
endpoint: https://api.sn.solsynth.dev
third_client_id: alphabot
atk: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NvbHN5bnRoLmRldiIsInN1YiI6IjEiLCJhdWQiOlsic29sYXItbmV0d29yayJdLCJleHAiOjE3MzgwODE2OTYsIm5iZiI6MTczODA3ODY5NiwiaWF0IjoxNzM4MDc4Njk2LCJqdGkiOiI1Yzg2MTYxZC00MTZjLTQwNDYtOWFlNS04YWZhNGIyZjdlMTkiLCJzZWQiOiIyMTciLCJ0eXAiOiJhY2Nlc3MifQ.LdLZ6FLb8IqPI__U8sT6VyxK5S_ZgwVGGL-tk01tK9C19wnbFFThPDgga1pJu_VVVpGVxzvMyd-3tBotzLMB5LjhYbtPOJakN2oug1HAgJ8zfc1clORlpHlUVisaiQtl3ZkWtzxni8etZDhJpqHU65IGQG01TO6PZGLgxKkMel4gGeeKhHpg9Q4Eewr3Pbl_wJkHVChJ9IJPmgioc_CACE10nEHZgwptCbndUz3AbIDOG9qW-7ZoprtKoRwAcuUXANK277VvdBRhwQjqKBREqVeMXP_Rv37jDPYhWpfS6HtKoHElZOTJG-69S2Zc3HyHlPJAPlzLAjGPoxScky79Gg
}
vars:secret [
atk,
third_client_tk
]

BIN
assets/icon/tray-icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icon/tray-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@ -241,6 +241,8 @@
"settingsMisc": "Misc",
"settingsMiscAbout": "About",
"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",
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"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",
"postShare": "Share",
"postShareImage": "Share via Image",
"postGetInsight": "Get Insight",
"postGetInsightTitle": "AI Insight",
"postGetInsightDescription": "AI may make mistakes, check important information.",
"appInitializing": "Initializing",
"poweredBy": "Powered by {}",
"shareIntent": "Share",
"shareIntentDescription": "What do you want to do with the content you are sharing?",
"shareIntentPostStory": "Post a Story",
"shareIntentSendChannel": "Share to Channel",
"updateAvailable": "Update Available",
"updateOngoing": "Updating, please wait...",
"custom": "Custom",
@ -571,6 +577,7 @@
"colorSchemeWhite": "White",
"colorSchemeBlack": "Black",
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
"postFeaturedComment": "Featured Comment",
"postCategoryTechnology": "Technology",
"postCategoryGaming": "Gaming",
"postCategoryLife": "Life",
@ -600,5 +607,8 @@
"walletCurrency": {
"one": "{} Source Point",
"other": "{} Source Points"
}
},
"aiThinkingProcess": "AI Thinking Process",
"accountSettingsApplied": "Account settings have been applied.",
"trayMenuExit": "Exit"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:croppy/croppy.dart';
@ -10,6 +11,7 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -40,6 +42,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
@ -206,7 +209,7 @@ class _AppSplashScreen extends StatefulWidget {
State<_AppSplashScreen> createState() => _AppSplashScreenState();
}
class _AppSplashScreenState extends State<_AppSplashScreen> {
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
@ -281,6 +284,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
final notify = context.read<NotificationProvider>();
notify.listen();
await notify.registerPushNotifications();
if (!mounted) return;
final sticker = context.read<SnStickerProvider>();
await sticker.listStickerEagerly();
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
@ -291,9 +297,45 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
await widgetUpdateRandomPost();
}
Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this);
await trayManager.setIcon(icon);
Menu menu = Menu(
items: [
MenuItem(
key: 'version_label',
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
disabled: true,
),
MenuItem.separator(),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
],
);
await trayManager.setContextMenu(menu);
}
AppLifecycleListener? _appLifecycleListener;
@override
void initState() {
super.initState();
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
_appLifecycleListener = AppLifecycleListener(
onExitRequested: _onExitRequested,
);
}
_trayInitialization();
_initialize().then((_) {
_postInitialization();
_tryRequestRating();
@ -301,6 +343,45 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
});
}
Future<AppExitResponse> _onExitRequested() async {
appWindow.hide();
return AppExitResponse.cancel;
}
@override
void onTrayIconMouseDown() {
if (Platform.isWindows) {
appWindow.show();
} else {
trayManager.popUpContextMenu();
}
}
@override
void onTrayIconRightMouseDown() {
if (Platform.isWindows) {
trayManager.popUpContextMenu();
} else {
appWindow.show();
}
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case 'exit':
_appLifecycleListener?.dispose();
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
break;
}
}
@override
void dispose() {
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) trayManager.removeListener(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();

View File

@ -12,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/notification.dart';
import 'package:tray_manager/tray_manager.dart';
class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
@ -71,22 +72,41 @@ class NotificationProvider extends ChangeNotifier {
);
}
int showingCount = 0;
List<SnNotification> notifications = List.empty(growable: true);
void listen() {
_ws.stream.stream.listen((event) {
if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!);
if (showingCount < 0) showingCount = 0;
showingCount++;
notifications.add(notification);
Future.delayed(const Duration(seconds: 3), () {
if (showingCount >= 0) showingCount--;
notifyListeners();
});
notifyListeners();
updateTray();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.lightImpact();
if (doHaptic) HapticFeedback.mediumImpact();
}
});
}
void updateTray() {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
if (notifications.isEmpty) {
trayManager.setTitle('');
} else {
trayManager.setTitle(' ${notifications.length.toString()}');
}
}
void clear() {
showingCount = 0;
notifications.clear();
updateTray();
notifyListeners();
}
}

View File

@ -9,6 +9,10 @@ class SnStickerProvider {
late final SnNetworkProvider _sn;
final Map<String, SnSticker?> _cache = {};
final Map<int, List<SnSticker>> stickersByPack = {};
List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
SnStickerProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
}
@ -17,6 +21,12 @@ class SnStickerProvider {
return _cache.containsKey(alias) && _cache[alias] == null;
}
void _cacheSticker(SnSticker sticker) {
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true);
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker);
}
Future<SnSticker?> lookupSticker(String alias) async {
if (_cache.containsKey(alias)) {
return _cache[alias];
@ -25,7 +35,7 @@ class SnStickerProvider {
try {
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
final sticker = SnSticker.fromJson(resp.data);
_cache[alias] = sticker;
_cacheSticker(sticker);
return sticker;
} catch (err) {
@ -35,4 +45,30 @@ class SnStickerProvider {
return null;
}
Future<void> listStickerEagerly() async {
var count = await listSticker();
for (var page = 1; count > 0; count -= 10) {
await listSticker(page: page);
page++;
}
}
Future<int> listSticker({int page = 0}) async {
try {
final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
'take': 10,
'offset': page * 10,
});
final data = resp.data;
final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
for (final sticker in stickers) {
_cacheSticker(sticker);
}
return data['count'] as int;
} catch (err) {
log('[Sticker] Failed to list stickers: $err');
rethrow;
}
}
}

View File

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

View File

@ -33,7 +33,16 @@ class WebSocketProvider extends ChangeNotifier {
await connect();
}
Completer<void>? _connectCompleter;
Future<void> connect({noRetry = false}) async {
if(_connectCompleter != null) {
await _connectCompleter!.future;
_connectCompleter = null;
}
_connectCompleter = Completer<void>();
if (!_ua.isAuthorized) return;
if (isConnected || conn != null) {
disconnect();
@ -64,12 +73,13 @@ class WebSocketProvider extends ChangeNotifier {
log('Retry connecting to websocket in 3 seconds...');
return Future.delayed(
const Duration(seconds: 3),
() => connect(noRetry: true),
() => connect(noRetry: true),
);
}
} finally {
isBusy = false;
notifyListeners();
_connectCompleter!.complete();
}
}

View File

@ -47,6 +47,7 @@ class HomeWidgetProvider {
}
Future<void> widgetUpdateRandomPost() async {
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
final snc = await SnNetworkProvider.createOffContextClient();
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
final post = SnPost.fromJson(resp.data['data'][0]);

View File

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

View File

@ -28,7 +28,19 @@ class AccountScreen extends StatelessWidget {
return AppScaffold(
appBar: AppBar(
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
? Stack(
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:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:intl/locale.dart';
class AccountSettingsScreen extends StatelessWidget {
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
Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
@ -21,6 +45,42 @@ class AccountSettingsScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(),

View File

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

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'package:dio/dio.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:styled_widget/styled_widget.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/chat_call.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/types/chat.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:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/user_directory.dart';
import '../../providers/userinfo.dart';
class ChatRoomScreenExtra {
final String? initialText;
final List<PostWriteMedia>? initialAttachments;
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
}
class ChatRoomScreen extends StatefulWidget {
final String scope;
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
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
_messageController = ChatMessageController(context);
_fetchChannel().then((_) async {
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>();

View File

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

View File

@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/notification.dart';
import 'package:surface/types/post.dart';
@ -21,6 +22,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../providers/userinfo.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 {
const NotificationScreen({super.key});
@ -36,15 +47,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
final List<SnNotification> _notifications = List.empty(growable: true);
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 {
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) return;
@ -53,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>();
final resp = await sn.client.get('/cgi/id/notifications?take=10');
_totalCount = resp.data['count'];
_notifications.addAll(
@ -61,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
.cast<SnNotification>() ??
[],
);
nty.updateTray();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -87,9 +91,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>();
final resp = await sn.client.put('/cgi/id/notifications/read/all');
_notifications.clear();
_fetchNotifications();
nty.clear();
if (!mounted) return;
context.showSnackbar(

View File

@ -20,13 +20,13 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:provider/provider.dart';
class PostEditorExtraProps {
class PostEditorExtra {
final String? text;
final String? title;
final String? description;
final List<PostWriteMedia>? attachments;
const PostEditorExtraProps({
const PostEditorExtra({
this.text,
this.title,
this.description,
@ -39,7 +39,7 @@ class PostEditorScreen extends StatefulWidget {
final int? postEditId;
final int? postReplyId;
final int? postRepostId;
final PostEditorExtraProps? extraProps;
final PostEditorExtra? extraProps;
const PostEditorScreen({
super.key,
@ -153,6 +153,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
),
]),
maxLines: 2,
),
actions: [
IconButton(
@ -250,121 +251,130 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 8),
child: Column(
children: [
// Replying Notice
if (_writeController.replyingPost != null)
Column(
children: [
ExpansionTile(
minTileHeight: 48,
leading: const Icon(Symbols.reply).padding(left: 4),
title: Text('postReplyingNotice')
.fontSize(15)
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
),
const Divider(height: 1),
],
),
// Reposting Notice
if (_writeController.repostingPost != null)
Column(
children: [
ExpansionTile(
minTileHeight: 48,
leading: const Icon(Symbols.forward).padding(left: 4),
title: Text('postRepostingNotice')
.fontSize(15)
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
children: <Widget>[
PostItem(
data: _writeController.repostingPost!,
)
child: Stack(
children: [
SingleChildScrollView(
padding: EdgeInsets.only(bottom: 160),
child: Column(
children: [
// Replying Notice
if (_writeController.replyingPost != null)
Column(
children: [
ExpansionTile(
minTileHeight: 48,
leading: const Icon(Symbols.reply).padding(left: 4),
title: Text('postReplyingNotice')
.fontSize(15)
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
),
const Divider(height: 1),
],
),
const Divider(height: 1),
],
),
// Editing Notice
if (_writeController.editingPost != null)
Column(
children: [
ExpansionTile(
minTileHeight: 48,
leading: const Icon(Symbols.edit_note).padding(left: 4),
title: Text('postEditingNotice')
.fontSize(15)
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
children: <Widget>[PostItem(data: _writeController.editingPost!)],
// Reposting Notice
if (_writeController.repostingPost != null)
Column(
children: [
ExpansionTile(
minTileHeight: 48,
leading: const Icon(Symbols.forward).padding(left: 4),
title: Text('postRepostingNotice')
.fontSize(15)
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
children: <Widget>[
PostItem(
data: _writeController.repostingPost!,
)
],
),
const Divider(height: 1),
],
),
const Divider(height: 1),
],
),
// Content Input Area
Container(
constraints: const BoxConstraints(maxWidth: 640),
child: TextField(
controller: _writeController.contentController,
maxLines: null,
decoration: InputDecoration(
hintText: 'fieldPostContent'.tr(),
hintStyle: TextStyle(fontSize: 14),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
// Editing Notice
if (_writeController.editingPost != null)
Column(
children: [
ExpansionTile(
minTileHeight: 48,
leading: const Icon(Symbols.edit_note).padding(left: 4),
title: Text('postEditingNotice')
.fontSize(15)
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
children: <Widget>[PostItem(data: _writeController.editingPost!)],
),
const Divider(height: 1),
],
),
// Content Input Area
Container(
constraints: const BoxConstraints(maxWidth: 640),
child: TextField(
controller: _writeController.contentController,
maxLines: null,
decoration: InputDecoration(
hintText: 'fieldPostContent'.tr(),
hintStyle: TextStyle(fontSize: 14),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
]
.expandIndexed(
(idx, ele) => [
if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
ele,
],
)
.toList(),
),
]
.expandIndexed(
(idx, ele) => [
if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
ele,
],
)
.toList(),
),
),
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: PostMediaPendingList(
thumbnail: _writeController.thumbnail,
attachments: _writeController.attachments,
isBusy: _writeController.isBusy,
onUpload: (int idx) async {
await _writeController.uploadSingleAttachment(context, idx);
},
onPostSetThumbnail: (int? idx) {
_writeController.setThumbnail(idx);
},
onInsertLink: (int idx) async {
_writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
},
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
_writeController.setIsBusy(true);
try {
_writeController.setAttachmentAt(idx, updatedMedia);
} finally {
_writeController.setIsBusy(false);
}
},
onRemove: (int idx) async {
_writeController.setIsBusy(true);
try {
_writeController.removeAttachmentAt(idx);
} finally {
_writeController.setIsBusy(false);
}
},
onUpdateBusy: (state) => _writeController.setIsBusy(state),
).padding(bottom: 8),
),
],
),
),
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
PostMediaPendingList(
thumbnail: _writeController.thumbnail,
attachments: _writeController.attachments,
isBusy: _writeController.isBusy,
onUpload: (int idx) async {
await _writeController.uploadSingleAttachment(context, idx);
},
onPostSetThumbnail: (int? idx) {
_writeController.setThumbnail(idx);
},
onInsertLink: (int idx) async {
_writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
},
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
_writeController.setIsBusy(true);
try {
_writeController.setAttachmentAt(idx, updatedMedia);
} finally {
_writeController.setIsBusy(false);
}
},
onRemove: (int idx) async {
_writeController.setIsBusy(true);
try {
_writeController.removeAttachmentAt(idx);
} finally {
_writeController.setIsBusy(false);
}
},
onUpdateBusy: (state) => _writeController.setIsBusy(state),
).padding(bottom: 8),
Material(
elevation: 2,
child: Column(

View File

@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.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:styled_widget/styled_widget.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/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 {
final Widget child;
@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
pathParameters: {
'mode': 'stories',
},
extra: PostEditorExtraProps(
extra: PostEditorExtra(
text: value
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path).join('\n'),
.map((e) => e.path)
.join('\n'),
attachments: value
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
.contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
),
);
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
void initState() {
super.initState();
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
_initialize();
_initialHandle();
}
@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
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:flutter/material.dart';
import 'package:gap/gap.dart';

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,23 @@
import 'dart:math' show min;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/universal_image.dart';
class ChatMessageInput extends StatefulWidget {
final ChatMessageController controller;
@ -46,6 +52,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
setState(() => _replyingMessage = value);
}
void setInitialText(String? value) {
_contentController.text = value ?? '';
setState(() {});
}
void setInitialAttachments(List<PostWriteMedia>? value) {
_attachments.addAll(value ?? []);
setState(() {});
}
void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? '';
_attachments.clear();
@ -134,6 +150,29 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final List<PostWriteMedia> _attachments = List.empty(growable: true);
OverlayEntry? _overlayEntry;
void _showEmojiPicker(BuildContext context) {
final overlay = Overlay.of(context);
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
bottom: 16 + MediaQuery.of(context).padding.bottom,
right: 16,
child: _StickerPicker(
originalText: _contentController.text,
onDismiss: () => _dismissEmojiPicker(),
onInsert: (str) => _contentController.text = str,
),
),
);
overlay.insert(_overlayEntry!);
}
void _dismissEmojiPicker() {
_overlayEntry?.remove();
}
@override
void dispose() {
_contentController.dispose();
@ -279,6 +318,19 @@ class ChatMessageInputState extends State<ChatMessageInput> {
),
),
const Gap(8),
IconButton(
icon: Icon(
Symbols.mood,
color: Theme.of(context).colorScheme.primary,
),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
onPressed: () {
_showEmojiPicker(context);
},
),
AddPostMediaButton(
onAdd: (items) {
setState(() {
@ -304,3 +356,105 @@ class ChatMessageInputState extends State<ChatMessageInput> {
);
}
}
class _StickerPicker extends StatelessWidget {
final String originalText;
final Function? onDismiss;
final Function(String)? onInsert;
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
@override
Widget build(BuildContext context) {
final sticker = context.read<SnStickerProvider>();
return GestureDetector(
onTap: () {
onDismiss?.call();
},
child: Container(
constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240),
child: Material(
elevation: 8,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ListView(
padding: EdgeInsets.zero,
children: sticker.stickersByPack.entries
.map((e) {
return <Widget>[
Container(
margin: EdgeInsets.only(bottom: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.value.first.pack.name).bold(),
Text(e.value.first.pack.description),
],
),
),
GridView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 48,
childAspectRatio: 1.0,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: e.value.length,
itemBuilder: (context, index) {
final sn = context.read<SnNetworkProvider>();
final element = e.value[index];
return GestureDetector(
onTap: () {
final withSpace = originalText.isNotEmpty;
onInsert?.call(
'$originalText${withSpace ? ' ' : ''}:${element.pack.prefix}${element.alias}:');
onDismiss?.call();
},
child: Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: ':${element.pack.prefix}${element.alias}:\n', style: GoogleFonts.robotoMono()),
TextSpan(text: element.name).bold(),
],
),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: UniversalImage(
sn.getAttachmentUrl(element.attachment.rid),
width: 48,
height: 48,
cacheHeight: 48,
cacheWidth: 48,
fit: BoxFit.contain,
),
),
),
),
);
},
),
];
})
.expand((ele) => ele)
.toList(),
),
),
),
),
);
}
}

View File

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

View File

@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler;
final Color? textColor;
final List<SnAttachment?>? attachments;
const MarkdownTextContent({
@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler,
this.textColor,
this.attachments,
});
@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
Theme.of(context),
).copyWith(
textScaler: textScaler,
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@ -126,14 +129,27 @@ class MarkdownTextContent extends StatelessWidget {
future: st.lookupSticker(alias),
builder: (context, snapshot) {
if (snapshot.hasData) {
return UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
fit: BoxFit.cover,
width: size,
height: size,
cacheHeight: size,
cacheWidth: size,
);
return GestureDetector(
child: UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
fit: BoxFit.contain,
width: size,
height: size,
cacheHeight: size,
cacheWidth: size,
),
onTap: () {
if (snapshot.data == null) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: [snapshot.data!.attachment],
initialIndex: 0,
heroTags: [const Uuid().v4()],
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
});
}
return const SizedBox.shrink();
},
@ -142,7 +158,7 @@ class MarkdownTextContent extends StatelessWidget {
);
case 'attachments':
final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1],
(ele) => ele?.rid == segments[1],
orElse: () => null,
);
if (attachment != null) {

View File

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

View File

@ -1,60 +1,181 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.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});
@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
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
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(
listenable: nty,
builder: (context, _) {
final current = nty.notifications.lastOrNull;
return IgnorePointer(
ignoring: !show,
child: GestureDetector(
child: Material(
elevation: 2,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer,
child: ua.isAuthorized
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
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,
child: Animate(
autoPlay: false,
controller: _animationController,
effects: [
SlideEffect(
begin: isMobile ? Offset(0, -1) : Offset(1, 0),
end: Offset(0, 0),
duration: Duration(milliseconds: 300),
curve: Curves.fastEaseInToSlowEaseOut,
),
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: () {
nty.clear();
if (current != null) {
_markOneAsRead(current);
}
},
),
);

View File

@ -1,10 +1,12 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.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/publisher_popover.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:xml/xml.dart';
class PostItem extends StatelessWidget {
final SnPost data;
@ -186,6 +189,7 @@ class PostItem extends StatelessWidget {
),
),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
_PostBottomAction(
data: data,
showComments: showComments,
@ -267,6 +271,7 @@ class PostItem extends StatelessWidget {
LinkPreviewWidget(
text: data.body['content'],
).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column(
@ -817,6 +822,22 @@ class _PostContentHeader extends StatelessWidget {
},
),
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(
onTap: onShare,
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 {
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) {
// ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err);

View File

@ -14,6 +14,7 @@
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@ -41,6 +42,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_linux
media_kit_video
pasteboard
tray_manager
url_launcher_linux
)

View File

@ -8,6 +8,7 @@ import Foundation
import bitsdojo_window_macos
import connectivity_plus
import device_info_plus
import file_picker
import file_saver
import file_selector_macos
import firebase_analytics
@ -28,6 +29,7 @@ import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import tray_manager
import url_launcher_macos
import video_compress
import wakelock_plus
@ -36,6 +38,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
@ -56,6 +59,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@ -8,6 +8,8 @@ PODS:
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_saver (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
@ -172,6 +174,8 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- tray_manager (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_compress (0.3.0):
@ -185,6 +189,7 @@ DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/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_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
@ -207,6 +212,7 @@ DEPENDENCIES:
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -237,6 +243,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_saver:
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
file_selector_macos:
@ -281,6 +289,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress:
@ -293,6 +303,7 @@ SPEC CHECKSUMS:
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
croppy: 25a638bd7d05411d8c697f481568f261037694fc
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
@ -328,6 +339,7 @@ SPEC CHECKSUMS:
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269

View File

@ -362,10 +362,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
url: "https://pub.dev"
source: hosted
version: "11.2.1"
version: "11.2.2"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -378,10 +378,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev"
source: hosted
version: "5.7.0"
version: "5.8.0+1"
dio_smart_retry:
dependency: "direct main"
description:
@ -394,10 +394,10 @@ packages:
dependency: transitive
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.1.0"
dismissible_page:
dependency: "direct main"
description:
@ -490,10 +490,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
url: "https://pub.dev"
source: hosted
version: "8.1.7"
version: "8.3.1"
file_saver:
dependency: "direct main"
description:
@ -764,10 +764,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
sha256: "46cdcdcd216f15ac04c80e24e814a89ea7143654442c53ba67fec349b4d44565"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
version: "0.7.6"
flutter_native_splash:
dependency: "direct dev"
description:
@ -886,10 +886,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: daf3ff5570f55396b2d2c9bf8136d7db3a8acf208ac0cef92a3ae2beb9a81550
sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
url: "https://pub.dev"
source: hosted
version: "14.7.1"
version: "14.7.2"
google_fonts:
dependency: "direct main"
description:
@ -1282,6 +1282,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.5"
menu_base:
dependency: transitive
description:
name: menu_base
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
meta:
dependency: transitive
description:
@ -1334,18 +1342,18 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254
url: "https://pub.dev"
source: hosted
version: "8.1.3"
version: "8.2.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.1.0"
pasteboard:
dependency: "direct main"
description:
@ -1710,10 +1718,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.5.1"
shared_preferences_android:
dependency: transitive
description:
@ -1778,6 +1786,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
shortid:
dependency: transitive
description:
name: shortid
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
url: "https://pub.dev"
source: hosted
version: "0.1.2"
sky_engine:
dependency: transitive
description: flutter
@ -1951,6 +1967,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
tray_manager:
dependency: "direct main"
description:
name: tray_manager
sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
typed_data:
dependency: transitive
description:
@ -2059,10 +2083,10 @@ packages:
dependency: transitive
description:
name: vector_graphics
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
sha256: a1870d398158844fe5db12441611ed9a2222ff4340258b539eaf3590c1b4bd7e
url: "https://pub.dev"
source: hosted
version: "1.1.15"
version: "1.1.17"
vector_graphics_codec:
dependency: transitive
description:
@ -2216,7 +2240,7 @@ packages:
source: hosted
version: "1.1.0"
xml:
dependency: transitive
dependency: "direct main"
description:
name: xml
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
# 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.
version: 2.2.2+59
version: 2.2.2+62
environment:
sdk: ^3.5.4
@ -117,6 +117,8 @@ dependencies:
cached_network_image: ^3.4.1
flutter_inappwebview: ^6.1.5
html: ^0.15.5
xml: ^6.5.0
tray_manager: ^0.3.2
dev_dependencies:
flutter_test:
@ -151,6 +153,8 @@ flutter:
- assets/icon/icon.png
- assets/icon/icon-dark.png
- assets/icon/icon-light-radius.png
- assets/icon/tray-icon.ico
- assets/icon/tray-icon.png
- assets/translations/
# An image asset can refer to one or more resolution-specific "variants", see

View File

@ -22,6 +22,7 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -57,6 +58,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
screen_brightness_windows
share_plus
tray_manager
url_launcher_windows
)