From ddd6ff7eee64ba94478a2e1a7f6981e47a82f56b Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Sat, 15 Mar 2025 15:38:50 +0800
Subject: [PATCH] :sparkles: Service status on home :wastebasket: Remove news
 from home

---
 assets/translations/en-US.json |  14 ++-
 assets/translations/zh-CN.json |  14 ++-
 assets/translations/zh-HK.json |  18 ++-
 assets/translations/zh-TW.json |  18 ++-
 lib/providers/sn_network.dart  |  14 +++
 lib/screens/home.dart          | 209 +++++++++++++++++++++------------
 6 files changed, 209 insertions(+), 78 deletions(-)

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<String, String> 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<HomeScreen> {
     ),
     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<String, dynamic>? _statuses;
+  ServiceStatus? _serviceStatus;
 
-  Future<void> _fetchArticle() async {
+  Future<void> _fetchStatuses() async {
     try {
       final sn = context.read<SnNetworkProvider>();
-      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),
               ),
-            )
+            ),
         ],
       ),
     );