diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index c28758c..390548a 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -795,5 +795,17 @@ "mixedFeed": "Mixed Feed", "mixedFeedDescription": "The Explore screen may not only display the user's posts, but may also contain other content. However, this mode does not apply to classification and filtering.", "filterFeed": "Exploring Adjust", - "feedUnknownItem": "Unable to display this content, the current version of the client does not support the type of content, please try to update the application afterwards." + "feedUnknownItem": "Unable to display this content, the current version of the client does not support the type of content, please try to update the application afterwards.", + "serviceStatusOperational": "All services operational", + "serviceStatusDowngraded": "Some services downgraded", + "serviceStatusFailed": "All services unavailable", + "serviceStatusFailedDescription": "The server is down or the maintenance is just finished.", + "serviceNameInsights": "Summarize and Insights", + "serviceNameInteractive": "Posts, Reactions and Explore", + "serviceNameReader": "News and Link Previews", + "serviceNameMessaging": "Chat", + "serviceNameMatrix": "Matrix Software and Game Marketplace", + "serviceNamePaperclip": "Attachments, Images and Files", + "serviceNameWallet": "Source Points Wallet", + "serviceNamePassport": "Authorization and Authentication" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index f697c3f..394311a 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -793,5 +793,17 @@ "mixedFeed": "混合推荐流", "mixedFeedDescription": "探索页面可能不只会展示用户的帖子,更可能包含其他的内容。但该模式不适用分类和过滤。", "filterFeed": "探索队列调整", - "feedUnknownItem": "无法显示该内容,当前版本客户端不支持该类型的内容,请尝试更新应用程序后再试。" + "feedUnknownItem": "无法显示该内容,当前版本客户端不支持该类型的内容,请尝试更新应用程序后再试。", + "serviceStatusOperational": "所有服务正常", + "serviceStatusDowngraded": "部分服务异常", + "serviceStatusFailed": "服务状态异常", + "serviceStatusFailedDescription": "服务器炸了或者刚刚执行完维护程序。", + "serviceNameInsights": "总结、见解与洞察", + "serviceNameInteractive": "帖子与互动", + "serviceNameReader": "新闻与链接展开", + "serviceNameMessaging": "即使聊天", + "serviceNameMatrix": "矩阵市场", + "serviceNamePaperclip": "附件", + "serviceNameWallet": "源点钱包", + "serviceNamePassport": "身份验证与授权" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 93a4847..13d9388 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -789,5 +789,21 @@ "fieldAccountStatusClearAt": "清除時間", "accountStatusNegative": "負面", "accountStatusNeutral": "中性", - "accountStatusPositive": "正面" + "accountStatusPositive": "正面", + "mixedFeed": "混合推薦流", + "mixedFeedDescription": "探索頁面可能不只會展示用户的帖子,更可能包含其他的內容。但該模式不適用分類和過濾。", + "filterFeed": "探索隊列調整", + "feedUnknownItem": "無法顯示該內容,當前版本客户端不支持該類型的內容,請嘗試更新應用程序後再試。", + "serviceStatusOperational": "所有服務正常", + "serviceStatusDowngraded": "部分服務異常", + "serviceStatusFailed": "服務狀態異常", + "serviceStatusFailedDescription": "服務器炸了或者剛剛執行完維護程序。", + "serviceNameInsights": "總結、見解與洞察", + "serviceNameInteractive": "帖子與互動", + "serviceNameReader": "新聞與鏈接展開", + "serviceNameMessaging": "即使聊天", + "serviceNameMatrix": "矩陣市場", + "serviceNamePaperclip": "附件", + "serviceNameWallet": "源點錢包", + "serviceNamePassport": "身份驗證與授權" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 7164139..dab4194 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -789,5 +789,21 @@ "fieldAccountStatusClearAt": "清除時間", "accountStatusNegative": "負面", "accountStatusNeutral": "中性", - "accountStatusPositive": "正面" + "accountStatusPositive": "正面", + "mixedFeed": "混合推薦流", + "mixedFeedDescription": "探索頁面可能不只會展示用戶的帖子,更可能包含其他的內容。但該模式不適用分類和過濾。", + "filterFeed": "探索隊列調整", + "feedUnknownItem": "無法顯示該內容,當前版本客戶端不支持該類型的內容,請嘗試更新應用程序後再試。", + "serviceStatusOperational": "所有服務正常", + "serviceStatusDowngraded": "部分服務異常", + "serviceStatusFailed": "服務狀態異常", + "serviceStatusFailedDescription": "服務器炸了或者剛剛執行完維護程序。", + "serviceNameInsights": "總結、見解與洞察", + "serviceNameInteractive": "帖子與互動", + "serviceNameReader": "新聞與鏈接展開", + "serviceNameMessaging": "即使聊天", + "serviceNameMatrix": "矩陣市場", + "serviceNamePaperclip": "附件", + "serviceNameWallet": "源點錢包", + "serviceNamePassport": "身份驗證與授權" } diff --git a/lib/providers/sn_network.dart b/lib/providers/sn_network.dart index 2467220..239294e 100644 --- a/lib/providers/sn_network.dart +++ b/lib/providers/sn_network.dart @@ -17,6 +17,20 @@ import 'package:synchronized/synchronized.dart'; import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart'; import 'package:talker_dio_logger/talker_dio_logger_settings.dart'; +enum ServiceStatus { operational, downgraded, failed } + +const Map kServicesName = { + 'ai': 'Insights', + 'co': 'Interactive', + 're': 'Reader', + 'im': 'Messaging', + 'ma': 'Matrix', + 'uc': 'Paperclip', + 'wa': 'Wallet', + 'id': 'Passport', + 'pusher': 'Pusher', +}; + const kNetworkServerDirectory = [ ('Solar Network', 'https://api.sn.solsynth.dev'), ('Local', 'http://localhost:8001'), diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 85cb625..4867c40 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -6,7 +6,6 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:html/parser.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; @@ -20,13 +19,14 @@ import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/widget.dart'; import 'package:surface/types/check_in.dart'; -import 'package:surface/types/news.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/updater.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class HomeScreenDashEntry { final String name; @@ -66,7 +66,7 @@ class _HomeScreenState extends State { ), HomeScreenDashEntry( name: 'dashEntryTodayNews', - child: _HomeDashTodayNews(), + child: _HomeDashServiceStatus(), cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2, ), ]; @@ -245,21 +245,31 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> { } } -class _HomeDashTodayNews extends StatefulWidget { - const _HomeDashTodayNews(); +class _HomeDashServiceStatus extends StatefulWidget { + const _HomeDashServiceStatus(); @override - State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState(); + State<_HomeDashServiceStatus> createState() => _HomeDashServiceStatusState(); } -class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { - SnNewsArticle? _article; +class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> { + Map? _statuses; + ServiceStatus? _serviceStatus; - Future _fetchArticle() async { + Future _fetchStatuses() async { try { final sn = context.read(); - final resp = await sn.client.get('/cgi/re/news/today'); - _article = SnNewsArticle.fromJson(resp.data['data']); + final resp = await sn.client.get('/directory/status'); + _statuses = resp.data; + if (_statuses!.values.contains(false)) { + if (_statuses!.values.contains(true)) { + _serviceStatus = ServiceStatus.downgraded; + } else { + _serviceStatus = ServiceStatus.failed; + } + } else { + _serviceStatus = ServiceStatus.operational; + } } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -272,7 +282,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { @override initState() { super.initState(); - _fetchArticle(); + _fetchStatuses(); } @override @@ -284,73 +294,124 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { children: [ Row( children: [ - const Icon(Symbols.newspaper), + const Icon(Symbols.flare), const Gap(8), - Text( - 'newsToday', - style: Theme.of(context).textTheme.titleLarge, - ).tr() - ], - ).padding(horizontal: 18, top: 12, bottom: 8), - if (_article != null) - Expanded( - child: InkWell( - borderRadius: BorderRadius.all(Radius.circular(8)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - Text( - _article!.title, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 18), - maxLines: - MediaQuery.of(context).size.width >= 640 ? 2 : 1, - overflow: TextOverflow.ellipsis, - ), - Text( - parse(_article!.description) - .children - .map((e) => e.text.trim()) - .join(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium, - ), - Builder(builder: (context) { - final date = _article!.publishedAt ?? _article!.createdAt; - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 2, - children: [ - Text(DateFormat().format(date)).textStyle( - Theme.of(context).textTheme.bodySmall!), - Text(' · ') - .textStyle(Theme.of(context).textTheme.bodySmall!) - .bold(), - Text(RelativeTime(context).format(date)).textStyle( - Theme.of(context).textTheme.bodySmall!), - ], - ).opacity(0.75); - }), - ], - ).padding(horizontal: 16), - onTap: () { - GoRouter.of(context).pushNamed( - 'newsDetail', - pathParameters: {'hash': _article!.hash}, - ); + Expanded( + child: Text( + 'serviceStatus', + style: Theme.of(context).textTheme.titleLarge, + ).tr(), + ), + IconButton( + icon: const Icon(Symbols.launch, size: 20), + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + onPressed: () { + launchUrlString('https://status.solsynth.dev'); }, ), - ) - else + ], + ).padding(horizontal: 18, top: 12, bottom: 8), + Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 6), + width: double.infinity, + color: _serviceStatus == null + ? Theme.of(context).colorScheme.surfaceContainerHigh + : switch (_serviceStatus) { + ServiceStatus.operational => Colors.green[300], + ServiceStatus.failed => Colors.red[300], + _ => Colors.orange[300], + }, + child: _serviceStatus == null + ? Row( + children: [ + const Icon( + Symbols.more_horiz, + size: 20, + ), + const Gap(10), + Text('serviceStatusOperational').tr(), + ], + ) + : switch (_serviceStatus) { + ServiceStatus.operational => Row( + children: [ + const Icon( + Symbols.check, + size: 20, + ), + const Gap(10), + Text('serviceStatusOperational').tr(), + ], + ), + ServiceStatus.failed => Tooltip( + message: 'serviceStatusFailedDescription'.tr(), + child: Row( + children: [ + const Icon( + Symbols.dangerous, + size: 20, + ), + const Gap(10), + Text('serviceStatusFailed').tr(), + ], + ), + ), + _ => Row( + children: [ + const Icon( + Symbols.error, + size: 20, + ), + const Gap(10), + Text('serviceStatusDowngraded').tr(), + ], + ), + }, + ), + if (_statuses != null) Expanded( - child: Center( - child: CircularProgressIndicator(), + child: SingleChildScrollView( + padding: EdgeInsets.only(top: 6), + child: Wrap( + spacing: 8, + children: [ + for (final entry in _statuses!.entries) + Tooltip( + message: kServicesName[entry.key] != null + ? 'serviceName${kServicesName[entry.key]}'.tr() + : 'unknown'.tr(), + child: Chip( + avatar: entry.value + ? const Icon( + Symbols.circle, + color: Colors.green, + fill: 1, + size: 16, + ) + : AnimateWidgetExtensions(const Icon( + Symbols.error, + color: Colors.red, + fill: 1, + size: 16, + )) + .animate(onPlay: (e) => e.repeat()) + .fadeIn( + duration: 500.ms, curve: Curves.easeOut) + .then() + .fadeOut( + duration: 500.ms, + delay: 1000.ms, + curve: Curves.easeIn, + ), + label: Text(kServicesName[entry.key] ?? entry.key), + ), + ), + ], + ).padding(horizontal: 12), ), - ) + ), ], ), );