Compare commits
6 Commits
2.2.2+59
...
b8dcdb2315
Author | SHA1 | Date | |
---|---|---|---|
|
b8dcdb2315 | ||
|
b7b921f1f4 | ||
|
319d5c7d7f | ||
|
4b5b001739 | ||
|
db8871a455 | ||
|
38dcaa6066 |
@@ -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"
|
||||
|
26
api/Passport/Developer Notify One User.bru
Normal file
26
api/Passport/Developer Notify One User.bru
Normal 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
|
||||
}
|
||||
}
|
11
api/WatchTower/Run Database Maintenance.bru
Normal file
11
api/WatchTower/Run Database Maintenance.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Run Database Maintenance
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/wt/maintenance/database
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
@@ -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
|
||||
]
|
||||
|
@@ -554,6 +554,9 @@
|
||||
"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",
|
||||
@@ -600,5 +603,6 @@
|
||||
"walletCurrency": {
|
||||
"one": "{} Source Point",
|
||||
"other": "{} Source Points"
|
||||
}
|
||||
},
|
||||
"aiThinkingProcess": "AI Thinking Process"
|
||||
}
|
||||
|
@@ -552,6 +552,9 @@
|
||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖图",
|
||||
"postGetInsight": "获取见解",
|
||||
"postGetInsightTitle": "AI 见解",
|
||||
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
@@ -598,5 +601,6 @@
|
||||
"walletCurrency": {
|
||||
"one": "{} 源点",
|
||||
"other": "{} 源点"
|
||||
}
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考过程"
|
||||
}
|
||||
|
@@ -552,6 +552,9 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
@@ -598,5 +601,6 @@
|
||||
"walletCurrency": {
|
||||
"one": "{} 源點",
|
||||
"other": "{} 源點"
|
||||
}
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考過程"
|
||||
}
|
||||
|
@@ -552,6 +552,9 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
@@ -598,5 +601,6 @@
|
||||
"walletCurrency": {
|
||||
"one": "{} 源點",
|
||||
"other": "{} 源點"
|
||||
}
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考過程"
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -71,22 +71,29 @@ 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();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.lightImpact();
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
notifications.clear();
|
||||
showingCount = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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(
|
||||
|
@@ -21,6 +21,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 +46,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;
|
||||
|
@@ -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';
|
||||
|
@@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@@ -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(
|
||||
|
@@ -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,
|
||||
),
|
||||
|
@@ -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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@@ -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,
|
||||
|
@@ -1,60 +1,184 @@
|
||||
import 'dart:math' show min;
|
||||
|
||||
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/config.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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -34,6 +35,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;
|
||||
@@ -817,6 +819,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(
|
||||
@@ -1181,3 +1199,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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
@@ -36,6 +37,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"))
|
||||
|
@@ -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):
|
||||
@@ -185,6 +187,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`)
|
||||
@@ -237,6 +240,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:
|
||||
@@ -293,6 +298,7 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
|
34
pubspec.lock
34
pubspec.lock
@@ -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:
|
||||
@@ -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:
|
||||
@@ -1334,10 +1334,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
|
||||
sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
version: "8.1.4"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1710,10 +1710,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:
|
||||
@@ -2059,10 +2059,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
|
||||
sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.15"
|
||||
version: "1.1.16"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2216,7 +2216,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
|
@@ -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+60
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -117,6 +117,7 @@ dependencies:
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_inappwebview: ^6.1.5
|
||||
html: ^0.15.5
|
||||
xml: ^6.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user