Compare commits

..

13 Commits

Author SHA1 Message Date
1153fbdeee Cache management 2025-02-27 23:46:47 +08:00
e933058338 💄 Optimize runtime log screen 2025-02-27 23:33:29 +08:00
ae9743c84f ♻️ Refactor logging module 2025-02-27 23:30:08 +08:00
32bf834108 Logging framework 2025-02-27 22:58:31 +08:00
1b41c847a6 Custom fonts 2025-02-27 22:35:12 +08:00
b1af6c2c97 🐛 Optimize and fix profile page loading issue 2025-02-27 22:11:53 +08:00
8e76ff3f84 Optimize user loading api usage 2025-02-27 20:51:47 +08:00
bd26602299 Code highlighting 2025-02-26 23:29:02 +08:00
52ab1d0d10 🐛 Fix chat last message displaying inconsistences 2025-02-26 00:29:35 +08:00
f746e06f65 ⚗️ Experimental user first badge showing on chat 2025-02-26 00:25:42 +08:00
d11069a2be 🐛 Bug fixes on notification page 2025-02-26 00:00:53 +08:00
d6dc487d9e Latex Rendering, closed #9 2025-02-25 23:49:48 +08:00
a07c7cdede 🐛 Fix infinite loading own sticker 2025-02-25 22:56:30 +08:00
34 changed files with 861 additions and 172 deletions

View File

@@ -0,0 +1,18 @@
meta {
name: Deal Abuse Report
type: http
seq: 3
}
put {
url: {{endpoint}}/cgi/id/reports/abuse/3/status
body: json
auth: inherit
}
body:json {
{
"status": "processed",
"message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。"
}
}

View File

@@ -203,6 +203,11 @@
"other": "{} comments" "other": "{} comments"
}, },
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsCustomFonts": "Custom Fonts",
"settingsCustomFontsDescription": "Set custom fonts for the application.",
"settingsCustomFontFamily": "Custom Font Family",
"settingsCustomFontFamilyHint": "Use comma to separate fonts, higher priority comes first",
"settingsCustomFontApplied": "Custom font has been applied.",
"settingsDisplayLanguage": "Display Language", "settingsDisplayLanguage": "Display Language",
"settingsDisplayLanguageDescription": "Set the application language.", "settingsDisplayLanguageDescription": "Set the application language.",
"settingsDisplayLanguageSystem": "Follow System", "settingsDisplayLanguageSystem": "Follow System",
@@ -722,5 +727,13 @@
"trayMenuMuteNotification": "Do Not Disturb", "trayMenuMuteNotification": "Do Not Disturb",
"update": "Update", "update": "Update",
"forceUpdate": "Force Update", "forceUpdate": "Force Update",
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available." "forceUpdateDescription": "Force to show the application update popup, even the new version is not available.",
"debugLogging": "Runtime Logs",
"runtimeLogsOpen": "Open Logs",
"runtimeLogsDescription": "Show the runtime logs to help debugging.",
"signinResetPasswordHint": "Please enter the username / email address to help us to find your account and reset your password.",
"cacheSize": "Cache Size",
"cacheDelete": "Clean Cache",
"cacheDeleteDescription": "Remove the cached images and other resources from your disk, the content will be downloaded from server again.",
"cacheDeleted": "All cache has been cleaned up."
} }

View File

@@ -201,6 +201,11 @@
"other": "{} 条评论" "other": "{} 条评论"
}, },
"settingsAppearance": "外观", "settingsAppearance": "外观",
"settingsCustomFonts": "自定义字体",
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
"settingsCustomFontFamily": "应用字体",
"settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
"settingsCustomFontApplied": "自定义字体已经应用。",
"settingsDisplayLanguage": "显示语言", "settingsDisplayLanguage": "显示语言",
"settingsDisplayLanguageDescription": "设置应用程序使用的语言", "settingsDisplayLanguageDescription": "设置应用程序使用的语言",
"settingsDisplayLanguageSystem": "跟随系统", "settingsDisplayLanguageSystem": "跟随系统",
@@ -720,5 +725,13 @@
"trayMenuMuteNotification": "静音通知", "trayMenuMuteNotification": "静音通知",
"update": "更新", "update": "更新",
"forceUpdate": "强制更新", "forceUpdate": "强制更新",
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。" "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "运行时日志",
"runtimeLogsOpen": "打开日志文件",
"runtimeLogsDescription": "显示运行时的日志记录。",
"signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。",
"cacheSize": "缓存资源大小",
"cacheDelete": "清除缓存",
"cacheDeleteDescription": "从磁盘中移除缓存的图片和其他资源,内容将从服务器重新下载。",
"cacheDeleted": "所有缓存已被清除。"
} }

View File

@@ -201,6 +201,11 @@
"other": "{} 條評論" "other": "{} 條評論"
}, },
"settingsAppearance": "外觀", "settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
"settingsCustomFontFamily": "應用字體",
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
"settingsCustomFontApplied": "自定義字體已經應用。",
"settingsDisplayLanguage": "顯示語言", "settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言", "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統", "settingsDisplayLanguageSystem": "跟隨系統",
@@ -720,5 +725,13 @@
"trayMenuMuteNotification": "靜音通知", "trayMenuMuteNotification": "靜音通知",
"update": "更新", "update": "更新",
"forceUpdate": "強制更新", "forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。",
"cacheSize": "緩存資源大小",
"cacheDelete": "清除緩存",
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
"cacheDeleted": "所有緩存已被清除。"
} }

View File

@@ -201,6 +201,11 @@
"other": "{} 條評論" "other": "{} 條評論"
}, },
"settingsAppearance": "外觀", "settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
"settingsCustomFontFamily": "應用字體",
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
"settingsCustomFontApplied": "自定義字體已經應用。",
"settingsDisplayLanguage": "顯示語言", "settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言", "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統", "settingsDisplayLanguageSystem": "跟隨系統",
@@ -720,5 +725,13 @@
"trayMenuMuteNotification": "靜音通知", "trayMenuMuteNotification": "靜音通知",
"update": "更新", "update": "更新",
"forceUpdate": "強制更新", "forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。",
"cacheSize": "緩存資源大小",
"cacheDelete": "清除緩存",
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
"cacheDeleted": "所有緩存已被清除。"
} }

View File

@@ -235,7 +235,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.49.0) - sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
@@ -445,7 +445,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -8,6 +7,7 @@ import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/database/database.dart'; import 'package:surface/database/database.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/database.dart'; import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@@ -532,7 +532,7 @@ class ChatMessageController extends ChangeNotifier {
}, },
).toJson(), ).toJson(),
)); ));
log('[Messaging] Send read event request: $_readEventAnchor'); logging.debug('[Messaging] Send read event request: $_readEventAnchor');
} }
@override @override

10
lib/logger.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'package:talker/talker.dart';
final logging = Talker(
settings: TalkerSettings(
enabled: true,
useHistory: true,
maxHistoryItems: 1000,
useConsoleLogs: true,
),
);

View File

@@ -20,6 +20,7 @@ import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/firebase_options.dart'; import 'package:surface/firebase_options.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
@@ -235,7 +236,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
await inAppReview.requestReview(); await inAppReview.requestReview();
prefs.setBool('rating_requested', true); prefs.setBool('rating_requested', true);
} else { } else {
log('Unable request app review, unavailable'); logging.error('Unable request app review, unavailable');
} }
} }
} else { } else {
@@ -263,17 +264,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
int.tryParse(remoteVersionString.split('+').last) ?? 0; int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = final localBuildNumber =
int.tryParse(localVersionString.split('+').last) ?? 0; int.tryParse(localVersionString.split('+').last) ?? 0;
log("[Update] Local: $localVersionString, Remote: $remoteVersionString"); logging.info(
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || if ((remoteVersion > localVersion ||
remoteBuildNumber > localBuildNumber) && remoteBuildNumber > localBuildNumber) &&
mounted) { mounted) {
final config = context.read<ConfigProvider>(); final config = context.read<ConfigProvider>();
config.setUpdate( config.setUpdate(
remoteVersionString, resp.data?['body'] ?? 'No changelog'); remoteVersionString, resp.data?['body'] ?? 'No changelog');
log("[Update] Update available: $remoteVersionString"); logging.info("[Update] Update available: $remoteVersionString");
} }
} catch (e) { } catch (e) {
log('[Error] Unable to check update: $e'); logging.error('[Error] Unable to check update...', e);
if (mounted) context.showErrorDialog('Unable to check update: $e'); if (mounted) context.showErrorDialog('Unable to check update: $e');
} }
} }
@@ -306,7 +308,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (!mounted) return; if (!mounted) return;
final sticker = context.read<SnStickerProvider>(); final sticker = context.read<SnStickerProvider>();
await sticker.listSticker(); await sticker.listSticker();
log('[Bootstrap] Everything initialized!'); logging.info('[Bootstrap] Everything initialized!');
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
await context.showErrorDialog(err); await context.showErrorDialog(err);

View File

@@ -18,6 +18,7 @@ const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link'; const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link'; const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view'; const kAppRealmCompactView = 'app_realm_compact_view';
const kAppCustomFonts = 'app_custom_fonts';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,

View File

@@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/link.dart'; import 'package:surface/types/link.dart';
@@ -20,7 +20,7 @@ class SnLinkPreviewProvider {
final target = b64.encode(url); final target = b64.encode(url);
if (_cache.containsKey(target)) return _cache[target]; if (_cache.containsKey(target)) return _cache[target];
log('[LinkPreview] Fetching $url ($target)'); logging.debug('[LinkPreview] Fetching $url ($target)');
try { try {
final resp = await _sn.client.get('/cgi/re/link/$target'); final resp = await _sn.client.get('/cgi/re/link/$target');
@@ -28,7 +28,7 @@ class SnLinkPreviewProvider {
_cache[url] = meta; _cache[url] = meta;
return meta; return meta;
} catch (err) { } catch (err) {
log('[LinkPreview] Failed to fetch $url ($target)...'); logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err);
return null; return null;
} }
} }

View File

@@ -1,4 +1,3 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
@@ -9,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
@@ -48,11 +48,13 @@ class NotificationProvider extends ChangeNotifier {
var deviceUuid = await FlutterUdid.consistentUdid; var deviceUuid = await FlutterUdid.consistentUdid;
if (deviceUuid.isEmpty) { if (deviceUuid.isEmpty) {
log("Unable to active push notifications, couldn't get device uuid"); logging.warning(
'[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
return; return;
} else { } else {
log('Device UUID is $deviceUuid'); logging.info('[Push Notification] Device UUID is $deviceUuid');
log('Registering device push notifications...'); logging
.info('[Push Notification] Registering device push notifications...');
} }
if (Platform.isIOS || Platform.isMacOS) { if (Platform.isIOS || Platform.isMacOS) {
@@ -62,7 +64,7 @@ class NotificationProvider extends ChangeNotifier {
provider = 'fcm'; provider = 'fcm';
token = await FirebaseMessaging.instance.getToken(); token = await FirebaseMessaging.instance.getToken();
} }
log('Device Push Token is $token'); logging.info('[Push Notification] Device Push Token is $token');
await _sn.client.post( await _sn.client.post(
'/cgi/id/notifications/subscription', '/cgi/id/notifications/subscription',

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -11,9 +10,12 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
import 'package:synchronized/synchronized.dart'; 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';
const kNetworkServerDirectory = [ const kNetworkServerDirectory = [
('Solar Network', 'https://api.sn.solsynth.dev'), ('Solar Network', 'https://api.sn.solsynth.dev'),
@@ -36,6 +38,17 @@ class SnNetworkProvider {
client = Dio(); client = Dio();
client.interceptors.add(
TalkerDioLogger(
talker: logging,
settings: const TalkerDioLoggerSettings(
printRequestHeaders: false,
printResponseHeaders: false,
printResponseMessage: true,
),
),
);
client.interceptors.add(RetryInterceptor( client.interceptors.add(RetryInterceptor(
dio: client, dio: client,
retries: 3, retries: 3,
@@ -69,7 +82,6 @@ class SnNetworkProvider {
_prefs = _config.prefs; _prefs = _config.prefs;
client.options.baseUrl = _config.serverUrl; client.options.baseUrl = _config.serverUrl;
}); });
} }
static Future<Dio> createOffContextClient() async { static Future<Dio> createOffContextClient() async {
@@ -91,7 +103,8 @@ class SnNetworkProvider {
RequestOptions options, RequestOptions options,
RequestInterceptorHandler handler, RequestInterceptorHandler handler,
) async { ) async {
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) { final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey),
prefs.getString(kRtkStoreKey), (atk, rtk) {
prefs.setString(kAtkStoreKey, atk); prefs.setString(kAtkStoreKey, atk);
prefs.setString(kRtkStoreKey, rtk); prefs.setString(kRtkStoreKey, rtk);
}); });
@@ -103,7 +116,8 @@ class SnNetworkProvider {
}, },
), ),
); );
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; client.options.baseUrl =
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
return client; return client;
} }
@@ -119,7 +133,8 @@ class SnNetworkProvider {
platformInfo = 'Web; ${deviceInfo.vendor}'; platformInfo = 'Web; ${deviceInfo.vendor}';
} else if (Platform.isAndroid) { } else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo; final deviceInfo = await DeviceInfoPlugin().androidInfo;
platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}'; platformInfo =
'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo; final deviceInfo = await DeviceInfoPlugin().iosInfo;
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}'; platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
@@ -128,7 +143,8 @@ class SnNetworkProvider {
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}'; platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo; final deviceInfo = await DeviceInfoPlugin().windowsInfo;
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; platformInfo =
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
final deviceInfo = await DeviceInfoPlugin().linuxInfo; final deviceInfo = await DeviceInfoPlugin().linuxInfo;
platformInfo = 'Linux; ${deviceInfo.prettyName}'; platformInfo = 'Linux; ${deviceInfo.prettyName}';
@@ -148,12 +164,15 @@ class SnNetworkProvider {
final tkLock = Lock(); final tkLock = Lock();
Future<String?> getFreshAtk() async { Future<String?> getFreshAtk() async {
return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) { return await _getFreshAtk(
client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey),
(atk, rtk) {
setTokenPair(atk, rtk); setTokenPair(atk, rtk);
}); });
} }
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async { static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk,
Function(String atk, String rtk)? onRefresh) async {
if (_refreshCompleter != null) { if (_refreshCompleter != null) {
return await _refreshCompleter!.future; return await _refreshCompleter!.future;
} else { } else {
@@ -185,7 +204,8 @@ class SnNetworkProvider {
final payload = b64.decode(rawPayload); final payload = b64.decode(rawPayload);
final exp = jsonDecode(payload)['exp']; final exp = jsonDecode(payload)['exp'];
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
log('Access token need refresh, doing it at ${DateTime.now()}'); logging.debug(
'[Auth] Access token need refresh, doing it at ${DateTime.now()}');
final result = await _refreshToken(client.options.baseUrl, rtk); final result = await _refreshToken(client.options.baseUrl, rtk);
if (result == null) { if (result == null) {
atk = null; atk = null;
@@ -199,12 +219,12 @@ class SnNetworkProvider {
_refreshCompleter!.complete(atk); _refreshCompleter!.complete(atk);
return atk; return atk;
} else { } else {
log('Access token refresh failed...'); logging.error('[Auth] Access token refresh failed...');
_refreshCompleter!.complete(null); _refreshCompleter!.complete(null);
} }
} }
} catch (err) { } catch (err) {
log('Failed to authenticate user: $err'); logging.error('[Auth] Failed to authenticate user...', err);
_refreshCompleter!.completeError(err); _refreshCompleter!.completeError(err);
} finally { } finally {
_refreshCompleter = null; _refreshCompleter = null;
@@ -237,7 +257,8 @@ class SnNetworkProvider {
return result.$1; return result.$1;
} }
static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async { static Future<(String, String)?> _refreshToken(
String baseUrl, String? rtk) async {
if (rtk == null) return null; if (rtk == null) return null;
final dio = Dio(); final dio = Dio();

View File

@@ -1,7 +1,6 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
@@ -51,7 +50,7 @@ class SnStickerProvider {
return sticker; return sticker;
} catch (err) { } catch (err) {
_cache[alias] = null; _cache[alias] = null;
log('[Sticker] Failed to lookup sticker $alias: $err'); logging.warning('[Sticker] Failed to lookup sticker $alias', err);
} }
return null; return null;
@@ -66,7 +65,7 @@ class SnStickerProvider {
_cacheSticker(sticker); _cacheSticker(sticker);
} }
} catch (err) { } catch (err) {
log('[Sticker] Failed to list stickers: $err'); logging.error('[Sticker] Failed to list stickers...', err);
rethrow; rethrow;
} }
} }

View File

@@ -13,8 +13,16 @@ class ThemeProvider extends ChangeNotifier {
}); });
} }
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) { void reloadTheme({
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) { Color? seedColorOverride,
bool? useMaterial3,
String? customFonts,
}) {
createAppThemeSet(
seedColorOverride: seedColorOverride,
useMaterial3: useMaterial3,
customFonts: customFonts,
).then((value) {
theme = value; theme = value;
notifyListeners(); notifyListeners();
}); });

View File

@@ -27,8 +27,11 @@ class UserDirectoryProvider {
plannedQuery.add(item); plannedQuery.add(item);
} }
} }
final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')}); if (plannedQuery.isEmpty) return out;
final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList(); final resp = await _sn.client
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
final respDecoded =
resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
var sideIdx = 0; var sideIdx = 0;
for (var idx = 0; idx < out.length; idx++) { for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue; if (out[idx] != null) continue;

View File

@@ -1,8 +1,7 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
@@ -30,8 +29,8 @@ class UserProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
refreshUser().then((value) async { refreshUser().then((value) async {
if (value != null) { if (value != null) {
log('Logged in as @${value.name}'); logging.info('[Auth] Logged in as @${value.name}');
log('Atk: ${await atk}'); logging.debug('[Auth] Access token: ${await atk}');
} }
}); });
} }

View File

@@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/websocket.dart'; import 'package:surface/types/websocket.dart';
@@ -30,7 +30,7 @@ class WebSocketProvider extends ChangeNotifier {
if (isConnected) return; if (isConnected) return;
if (!_ua.isAuthorized) return; if (!_ua.isAuthorized) return;
log('[WebSocket] Connecting to the server...'); logging.debug('[WebSocket] Connecting to the server...');
await connect(); await connect();
} }
@@ -62,17 +62,14 @@ class WebSocketProvider extends ChangeNotifier {
await conn!.ready; await conn!.ready;
_wsStream = conn!.stream.asBroadcastStream(); _wsStream = conn!.stream.asBroadcastStream();
listen(); listen();
log('[WebSocket] Connected to server!'); logging.info('[WebSocket] Connected to server!');
isConnected = true; isConnected = true;
} catch (err) { } catch (err) {
if (err is WebSocketChannelException) { logging.error('[WebSocket] Failed to connect to websocket...', err);
log('Failed to connect to websocket: ${(err.inner as dynamic).message}');
} else {
log('Failed to connect to websocket: $err');
}
if (!noRetry) { if (!noRetry) {
log('Retry connecting to websocket in 3 seconds...'); logging.warning(
'[WebSocket] Retry connecting to websocket in 3 seconds...');
return Future.delayed( return Future.delayed(
const Duration(seconds: 3), const Duration(seconds: 3),
() => connect(noRetry: true), () => connect(noRetry: true),
@@ -100,7 +97,8 @@ class WebSocketProvider extends ChangeNotifier {
_wsStream!.listen( _wsStream!.listen(
(event) { (event) {
final packet = WebSocketPackage.fromJson(jsonDecode(event)); final packet = WebSocketPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}'); logging.debug(
'[Websocket] Incoming message: ${packet.method} ${packet.message}');
pk.sink.add(packet); pk.sink.add(packet);
}, },
onDone: () { onDone: () {

View File

@@ -21,6 +21,7 @@ import 'package:surface/screens/chat/room.dart';
import 'package:surface/screens/explore.dart'; import 'package:surface/screens/explore.dart';
import 'package:surface/screens/friend.dart'; import 'package:surface/screens/friend.dart';
import 'package:surface/screens/home.dart'; import 'package:surface/screens/home.dart';
import 'package:surface/screens/logging.dart';
import 'package:surface/screens/news/news_detail.dart'; import 'package:surface/screens/news/news_detail.dart';
import 'package:surface/screens/news/news_list.dart'; import 'package:surface/screens/news/news_list.dart';
import 'package:surface/screens/notification.dart'; import 'package:surface/screens/notification.dart';
@@ -249,6 +250,11 @@ final _appRoutes = [
), ),
], ],
), ),
GoRoute(
path: '/debug/logging',
name: 'debugLogging',
builder: (context, state) => const DebugLoggingScreen(),
),
GoRoute( GoRoute(
path: '/album', path: '/album',
name: 'album', name: 'album',

View File

@@ -43,7 +43,8 @@ class UserScreen extends StatefulWidget {
State<UserScreen> createState() => _UserScreenState(); State<UserScreen> createState() => _UserScreenState();
} }
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin { class _UserScreenState extends State<UserScreen>
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
SnAccount? _account; SnAccount? _account;
@@ -64,13 +65,18 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
} }
} }
Future<List<SnCheckInRecord>> _getCheckInRecords() async { List<SnCheckInRecord>? _records;
Future<void> _getCheckInRecords() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14'); final resp =
return List.from( await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], setState(() {
); _records = List.from(
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
);
});
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
rethrow; rethrow;
@@ -98,7 +104,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Future<void> _fetchPublishers() async { Future<void> _fetchPublishers() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}'); final resp =
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
_publishers = List<SnPublisher>.from( _publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
@@ -144,7 +151,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
'related': _account!.name, 'related': _account!.name,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); context.showSnackbar(
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -160,9 +168,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {}); await rel.updateRelationship(
_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); context.showSnackbar(
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -188,12 +198,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
double _appBarBlur = 0.0; double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble(); late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
setState(() { setState(() {
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); _appBarBlur =
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
}); });
} }
@@ -205,6 +217,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
_fetchStatus(); _fetchStatus();
_fetchPublishers(); _fetchPublishers();
_getCheckInRecords();
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
@@ -260,18 +273,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _account!.nick, text: _account!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith( style:
color: Colors.white, Theme.of(context).textTheme.titleLarge!.copyWith(
shadows: labelShadows, color: Colors.white,
), shadows: labelShadows,
),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: '@${_account!.name}', text: '@${_account!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style:
color: Colors.white, Theme.of(context).textTheme.bodySmall!.copyWith(
shadows: labelShadows, color: Colors.white,
), shadows: labelShadows,
),
), ),
]), ]),
), ),
@@ -339,7 +354,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
PopupMenuButton( PopupMenuButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: ButtonStyle( style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
), ),
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
@@ -399,7 +415,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey, color: (_status?.isOnline ?? false)
? Colors.green
: Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text( Text(
@@ -409,7 +427,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
: 'accountStatusOffline'.tr() : 'accountStatusOffline'.tr()
: 'loading'.tr(), : 'loading'.tr(),
), ),
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null) if (_status != null &&
!_status!.isOnline &&
_status!.lastSeenAt != null)
Text( Text(
'accountStatusLastSeen'.tr(args: [ 'accountStatusLastSeen'.tr(args: [
_status!.lastSeenAt != null _status!.lastSeenAt != null
@@ -429,11 +449,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
(ele) => Tooltip( (ele) => Tooltip(
richMessage: TextSpan( richMessage: TextSpan(
children: [ children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), TextSpan(
text: kBadgesMeta[ele.type]?.$1.tr() ??
'unknown'.tr()),
if (ele.metadata['title'] != null) if (ele.metadata['title'] != null)
TextSpan( TextSpan(
text: '\n${ele.metadata['title']}', text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(
fontWeight: FontWeight.bold),
), ),
TextSpan(text: '\n'), TextSpan(text: '\n'),
TextSpan( TextSpan(
@@ -442,7 +465,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
], ],
), ),
child: Icon( child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, kBadgesMeta[ele.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3, color: kBadgesMeta[ele.type]?.$3,
fill: 1, fill: 1,
), ),
@@ -458,7 +482,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [ children: [
const Icon(Symbols.calendar_add_on), const Icon(Symbols.calendar_add_on),
const Gap(8), const Gap(8),
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]), Text('publisherJoinedAt').tr(args: [
DateFormat('y/M/d').format(_account!.createdAt)
]),
], ],
), ),
Row( Row(
@@ -491,17 +517,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [ children: [
const Icon(Symbols.star), const Icon(Symbols.star),
const Gap(8), const Gap(8),
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'), Text(
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
const Gap(8), const Gap(8),
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5), Text(calcLevelUpProgressLevel(
_account?.profile?.experience ?? 0))
.fontSize(11)
.opacity(0.5),
const Gap(8), const Gap(8),
Container( Container(
width: double.infinity, width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160), constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: calcLevelUpProgress(_account?.profile?.experience ?? 0), value: calcLevelUpProgress(
_account?.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer, backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainer,
).alignment(Alignment.centerLeft), ).alignment(Alignment.centerLeft),
), ),
], ],
@@ -514,21 +547,23 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
SliverToBoxAdapter(child: const Divider()), SliverToBoxAdapter(child: const Divider()),
const SliverGap(12), const SliverGap(12),
SliverToBoxAdapter( SliverToBoxAdapter(
child: FutureBuilder<List<SnCheckInRecord>>( child: Builder(
future: _getCheckInRecords(), builder: (context) {
builder: (context, snapshot) { if (_records == null) return const SizedBox.shrink();
if (!snapshot.hasData) return const SizedBox.shrink(); if (_records!.length <= 1) {
if (snapshot.data!.length <= 1) {
return Text( return Text(
'accountCheckInNoRecords', 'accountCheckInNoRecords',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8); )
.tr()
.fontWeight(FontWeight.bold)
.center()
.padding(horizontal: 20, vertical: 8);
} }
final records = snapshot.data!;
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 240, height: 240,
child: CheckInRecordChart(records: records), child: CheckInRecordChart(records: _records!),
).padding( ).padding(
right: 24, right: 24,
left: 16, left: 16,
@@ -544,7 +579,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('accountBadge')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
SizedBox( SizedBox(
height: 80, height: 80,
width: double.infinity, width: double.infinity,
@@ -558,7 +597,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Card( child: Card(
child: ListTile( child: ListTile(
leading: Icon( leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark, kBadgesMeta[badge.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[badge.type]?.$3, color: kBadgesMeta[badge.type]?.$3,
fill: 1, fill: 1,
), ),
@@ -568,7 +608,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
subtitle: badge.metadata['title'] != null subtitle: badge.metadata['title'] != null
? Text(badge.metadata['title']) ? Text(badge.metadata['title'])
: Text( : Text(
DateFormat('y/M/d').format(badge.createdAt), DateFormat('y/M/d')
.format(badge.createdAt),
), ),
), ),
), ),
@@ -664,7 +705,8 @@ class CheckInRecordChart extends StatelessWidget {
), ),
) )
.toList(), .toList(),
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh, getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
), ),
titlesData: FlTitlesData( titlesData: FlTitlesData(

View File

@@ -74,18 +74,20 @@ class _ChatScreenState extends State<ChatScreen> {
if (!mounted) return; if (!mounted) return;
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
final idSet = <int>{};
for (final channel in channels) { for (final channel in channels) {
if (channel.type == 1) { if (channel.type == 1) {
await ud.listAccount( idSet.addAll(
channel.members channel.members
?.cast<SnChannelMember?>() ?.cast<SnChannelMember?>()
.map((ele) => ele?.accountId) .map((ele) => ele?.accountId)
.where((ele) => ele != null) .where((ele) => ele != null)
.toSet() ?? .cast<int>() ??
{}, [],
); );
} }
} }
if (idSet.isNotEmpty) await ud.listAccount(idSet);
if (mounted) setState(() => _channels = channels); if (mounted) setState(() => _channels = channels);
}) })
@@ -290,10 +292,34 @@ class _ChatScreenState extends State<ChatScreen> {
], ],
), ),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Row(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', children: [
maxLines: 1, Expanded(
overflow: TextOverflow.ellipsis, child: Text(
lastMessage.body['text'] ??
'Unable preview',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(4),
Text(
DateFormat(
lastMessage.createdAt.toLocal().day ==
DateTime.now().day
? 'HH:mm'
: lastMessage.createdAt
.toLocal()
.year ==
DateTime.now().year
? 'MM/dd'
: 'yy/MM/dd',
).format(lastMessage.createdAt.toLocal()),
style: GoogleFonts.robotoMono(
fontSize: 12,
),
),
],
) )
: Text( : Text(
channel.description, channel.description,
@@ -336,6 +362,8 @@ class _ChatScreenState extends State<ChatScreen> {
'unknown'.tr()), 'unknown'.tr()),
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.primary,
textColor:
Theme.of(context).colorScheme.onPrimary,
), ),
const Gap(6), const Gap(6),
Expanded( Expanded(
@@ -346,6 +374,7 @@ class _ChatScreenState extends State<ChatScreen> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const Gap(4),
Text( Text(
DateFormat( DateFormat(
lastMessage.createdAt.toLocal().day == lastMessage.createdAt.toLocal().day ==

167
lib/screens/logging.dart Normal file
View File

@@ -0,0 +1,167 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/logger.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:talker_dio_logger/dio_logs.dart';
import 'package:talker_flutter/talker_flutter.dart';
final Map<LogLevel, IconData> kLogLevelIcons = {
LogLevel.error: Symbols.error,
LogLevel.critical: Symbols.error,
LogLevel.warning: Symbols.warning,
LogLevel.info: Symbols.info,
LogLevel.debug: Symbols.info_i,
LogLevel.verbose: Symbols.info_i,
};
final Map<LogLevel, bool> kLogLevelFilled = {
LogLevel.error: false,
LogLevel.critical: true,
LogLevel.warning: true,
LogLevel.info: true,
LogLevel.debug: false,
LogLevel.verbose: false,
};
class DebugLoggingScreen extends StatelessWidget {
const DebugLoggingScreen({super.key});
@override
Widget build(BuildContext context) {
final talkerTheme = TalkerScreenTheme.fromTheme(Theme.of(context));
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('debugLogging').tr(),
actions: [
IconButton(
onPressed: () {
logging.cleanHistory();
Navigator.pop(context);
},
icon: const Icon(Symbols.delete),
),
],
),
body: ListView.builder(
reverse: true,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
itemCount: logging.history.length,
itemBuilder: (context, index) {
final log = logging.history[index];
final color = log.getFlutterColor(talkerTheme);
return ListTile(
minTileHeight: 0,
tileColor: color.withOpacity(0.2),
leading: Icon(
kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help,
color: color,
fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false)
? 1
: 0,
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (log is DioRequestLog)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${log.requestOptions.method} ${log.displayMessage}',
style: GoogleFonts.robotoMono(fontSize: 13),
),
if (log.requestOptions.data != null)
Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
title: Text('Payload').fontSize(13),
minTileHeight: 0,
tilePadding: EdgeInsets.zero,
expandedCrossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
log.requestOptions.data.toString(),
style: GoogleFonts.robotoMono(fontSize: 13),
),
],
),
),
],
)
else if (log is DioResponseLog)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${log.response.statusCode} ${log.displayMessage}',
style: GoogleFonts.robotoMono(fontSize: 13),
),
if (log.response.data != null)
Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
title: Text('Payload').fontSize(13),
minTileHeight: 0,
tilePadding: EdgeInsets.zero,
expandedCrossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
log.response.data.toString(),
style: GoogleFonts.robotoMono(fontSize: 13),
),
],
),
),
],
)
else
Text(
log.displayMessage,
style: GoogleFonts.robotoMono(fontSize: 13),
),
if (log.exception != null)
Text(
log.displayException,
style: GoogleFonts.robotoMono(fontSize: 13),
).bold(),
if (log.error != null)
Text(
log.displayException,
style: GoogleFonts.robotoMono(fontSize: 13),
).bold(),
if (log.stackTrace != null)
Text(
log.displayStackTrace,
style: GoogleFonts.robotoMono(fontSize: 12),
).padding(top: 4),
],
),
subtitle: Text(
'${(log.title?.replaceAll('-', ' ') ?? 'default').capitalizeEachWord()} · ${log.displayTime()}',
).fontSize(11),
onTap: () {
Clipboard.setData(
ClipboardData(
text: log.generateTextMessage(),
),
);
},
);
},
),
);
}
}

View File

@@ -29,6 +29,7 @@ const Map<String, IconData> kNotificationTopicIcons = {
'passport.security.otp': Symbols.password, 'passport.security.otp': Symbols.password,
'interactive.subscription': Symbols.subscriptions, 'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction, 'interactive.feedback': Symbols.add_reaction,
'interactive.reply': Symbols.reply,
'messaging.callStart': Symbols.call_received, 'messaging.callStart': Symbols.call_received,
'wallet.transaction.new': Symbols.receipt, 'wallet.transaction.new': Symbols.receipt,
}; };
@@ -57,10 +58,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>(); final nty = context.read<NotificationProvider>();
final resp = await sn.client.get('/cgi/id/notifications?take=10'); final resp =
await sn.client.get('/cgi/id/notifications', queryParameters: {
'take': 10,
'offset': _notifications.length,
});
_totalCount = resp.data['count']; _totalCount = resp.data['count'];
_notifications.addAll( _notifications.addAll(
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [], resp.data['data']
?.map((e) => SnNotification.fromJson(e))
.cast<SnNotification>() ??
[],
); );
nty.updateTray(); nty.updateTray();
} catch (err) { } catch (err) {
@@ -186,7 +194,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
_fetchNotifications(); _fetchNotifications();
}, },
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!, hasReachedMax: _totalCount != null &&
_notifications.length >= _totalCount!,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final nty = _notifications[idx]; final nty = _notifications[idx];
return Row( return Row(
@@ -218,13 +227,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
isAutoWarp: true, isAutoWarp: true,
), ),
), ),
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription'] if ([
.contains(nty.topic) && 'interactive.reply',
'interactive.feedback',
'interactive.subscription'
].contains(nty.topic) &&
nty.metadata['related_post'] != null) nty.metadata['related_post'] != null)
GestureDetector( GestureDetector(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(
Radius.circular(8)),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@@ -243,7 +256,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postDetail', 'postDetail',
pathParameters: { pathParameters: {
'slug': nty.metadata['related_post']!['id'].toString(), 'slug': nty
.metadata['related_post']!['id']
.toString(),
}, },
); );
}, },
@@ -272,8 +287,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
IconButton( IconButton(
icon: const Icon(Symbols.check), icon: const Icon(Symbols.check),
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4), visualDensity:
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty), const VisualDensity(horizontal: -4, vertical: -4),
onPressed:
_isSubmitting ? null : () => _markOneAsRead(nty),
), ),
], ],
).padding(horizontal: 16); ).padding(horizontal: 16);

View File

@@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@@ -48,6 +49,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
late final SharedPreferences _prefs; late final SharedPreferences _prefs;
String _docBasepath = '/'; String _docBasepath = '/';
final TextEditingController _customFontController = TextEditingController();
final TextEditingController _serverUrlController = TextEditingController(); final TextEditingController _serverUrlController = TextEditingController();
@override @override
@@ -62,11 +64,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
final config = context.read<ConfigProvider>(); final config = context.read<ConfigProvider>();
_prefs = config.prefs; _prefs = config.prefs;
_serverUrlController.text = config.serverUrl; _serverUrlController.text = config.serverUrl;
if (_prefs.getString(kAppCustomFonts) != null) {
_customFontController.text = _prefs.getString(kAppCustomFonts) ?? '';
}
} }
@override @override
void dispose() { void dispose() {
_serverUrlController.dispose(); _serverUrlController.dispose();
_customFontController.dispose();
super.dispose(); super.dispose();
} }
@@ -330,6 +336,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); setState(() {});
}, },
), ),
ListTile(
leading: const Icon(Symbols.font_download),
title: Text('settingsCustomFonts').tr(),
subtitle: Text('settingsCustomFontsDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 14),
trailing: IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(Icons.clear),
onPressed: () {
_prefs.remove(kAppCustomFonts);
context.showSnackbar('settingsCustomFontApplied'.tr());
final theme = context.read<ThemeProvider>();
_customFontController.clear();
theme.reloadTheme();
},
),
),
TextField(
controller: _customFontController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'settingsCustomFontFamily'.tr(),
helperText: 'settingsCustomFontFamilyHint'.tr(),
prefixIcon: const Icon(Symbols.format_paint),
suffixIcon: IconButton(
icon: const Icon(Symbols.save),
onPressed: () {
_prefs.setString(
kAppCustomFonts,
_customFontController.text,
);
context.showSnackbar('settingsCustomFontApplied'.tr());
final theme = context.read<ThemeProvider>();
theme.reloadTheme();
},
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16, top: 8, bottom: 4),
], ],
), ),
Column( Column(
@@ -534,6 +581,37 @@ class _SettingsScreenState extends State<SettingsScreen> {
.fontSize(17) .fontSize(17)
.tr() .tr()
.padding(horizontal: 20, bottom: 4), .padding(horizontal: 20, bottom: 4),
ListTile(
leading: const Icon(Symbols.home_storage),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('cacheSize').tr(),
subtitle: FutureBuilder(
future: DefaultCacheManager().store.getCacheSize(),
builder: (context, snapshot) {
if (!snapshot.hasData || kIsWeb) {
return Text('unknown').tr();
}
return Text(
snapshot.data!.formatBytes(),
style: GoogleFonts.robotoMono(),
);
},
),
),
ListTile(
leading: const Icon(Symbols.cleaning_services),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('cacheDelete').tr(),
subtitle: Text('cacheDeleteDescription').tr(),
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
await DefaultCacheManager().emptyCache();
if (!context.mounted) return;
HapticFeedback.heavyImpact();
context.showSnackbar('cacheDeleted'.tr());
setState(() {});
},
),
ListTile( ListTile(
leading: const Icon(Symbols.database), leading: const Icon(Symbols.database),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
@@ -618,6 +696,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
}, },
), ),
ListTile(
title: Text('runtimeLogsOpen').tr(),
subtitle: Text('runtimeLogsDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.receipt_long),
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
GoRouter.of(context).pushNamed('debugLogging');
},
),
ListTile( ListTile(
title: Text('settingsMiscAbout').tr(), title: Text('settingsMiscAbout').tr(),
subtitle: Text('settingsMiscAboutDescription').tr(), subtitle: Text('settingsMiscAboutDescription').tr(),

View File

@@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen>
child: InfiniteList( child: InfiniteList(
itemCount: _packs.length, itemCount: _packs.length,
onFetchData: _fetchPacks, onFetchData: _fetchPacks,
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!, hasReachedMax:
(_totalCount != null && _packs.length >= _totalCount!) ||
_tabController.index == 2,
isLoading: _isBusy, isLoading: _isBusy,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final pack = _packs[idx]; final pack = _packs[idx];

View File

@@ -11,10 +11,19 @@ class ThemeSet {
ThemeSet({required this.light, required this.dark}); ThemeSet({required this.light, required this.dark});
} }
Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async { Future<ThemeSet> createAppThemeSet(
{Color? seedColorOverride, bool? useMaterial3, String? customFonts}) async {
return ThemeSet( return ThemeSet(
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3), light: await createAppTheme(
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3), Brightness.light,
useMaterial3: useMaterial3,
customFonts: customFonts,
),
dark: await createAppTheme(
Brightness.dark,
useMaterial3: useMaterial3,
customFonts: customFonts,
),
); );
} }
@@ -22,24 +31,35 @@ Future<ThemeData> createAppTheme(
Brightness brightness, { Brightness brightness, {
Color? seedColorOverride, Color? seedColorOverride,
bool? useMaterial3, bool? useMaterial3,
String? customFonts,
}) async { }) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey); final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo; final seedColor =
seedColorString != null ? Color(seedColorString) : Colors.indigo;
final colorScheme = ColorScheme.fromSeed( final colorScheme = ColorScheme.fromSeed(
seedColor: seedColorOverride ?? seedColor, seedColor: seedColorOverride ?? seedColor,
brightness: brightness, brightness: brightness,
); );
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarTransparent =
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 =
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
?.split(',')
.map((ele) => ele.trim())
.toList();
return ThemeData( return ThemeData(
useMaterial3: useM3, useMaterial3: useM3,
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
fontFamily: inUseFonts?.firstOrNull,
fontFamilyFallback: inUseFonts?.sublist(1),
iconTheme: IconThemeData( iconTheme: IconThemeData(
fill: 0, fill: 0,
weight: 400, weight: 400,
@@ -52,8 +72,10 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarTransparent ? 0 : null, elevation: hasAppBarTransparent ? 0 : null,
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary, backgroundColor:
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary, hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
foregroundColor:
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
pageTransitionsTheme: PageTransitionsTheme( pageTransitionsTheme: PageTransitionsTheme(
builders: { builders: {

View File

@@ -10,6 +10,7 @@ class AccountImage extends StatelessWidget {
final Color? foregroundColor; final Color? foregroundColor;
final double? radius; final double? radius;
final Widget? fallbackWidget; final Widget? fallbackWidget;
final Widget? badge;
const AccountImage({ const AccountImage({
super.key, super.key,
@@ -18,6 +19,7 @@ class AccountImage extends StatelessWidget {
this.foregroundColor, this.foregroundColor,
this.radius, this.radius,
this.fallbackWidget, this.fallbackWidget,
this.badge,
}); });
@override @override
@@ -26,26 +28,36 @@ class AccountImage extends StatelessWidget {
final url = sn.getAttachmentUrl(content ?? ''); final url = sn.getAttachmentUrl(content ?? '');
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return CircleAvatar( return Stack(
key: Key('attachment-${content.hashCode}'), children: [
radius: radius, CircleAvatar(
backgroundColor: backgroundColor, key: Key('attachment-${content.hashCode}'),
backgroundImage: (content?.isNotEmpty ?? false) radius: radius,
? ResizeImage( backgroundColor: backgroundColor,
UniversalImage.provider(url), backgroundImage: (content?.isNotEmpty ?? false)
width: ((radius ?? 20) * devicePixelRatio * 2).round(), ? ResizeImage(
height: ((radius ?? 20) * devicePixelRatio * 2).round(), UniversalImage.provider(url),
policy: ResizeImagePolicy.fit, width: ((radius ?? 20) * devicePixelRatio * 2).round(),
) height: ((radius ?? 20) * devicePixelRatio * 2).round(),
: null, policy: ResizeImagePolicy.fit,
child: (content?.isEmpty ?? true) )
? (fallbackWidget ?? : null,
Icon( child: (content?.isEmpty ?? true)
Symbols.account_circle, ? (fallbackWidget ??
size: radius != null ? radius! * 1.2 : 24, Icon(
color: foregroundColor, Symbols.account_circle,
)) size: radius != null ? radius! * 1.2 : 24,
: null, color: foregroundColor,
))
: null,
),
if (badge != null)
Positioned(
right: -4,
bottom: -4,
child: badge!,
),
],
); );
} }
} }

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@@ -8,6 +7,7 @@ import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/logger.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
@@ -17,10 +17,12 @@ class PendingVideoCompressDialog extends StatefulWidget {
const PendingVideoCompressDialog({super.key, required this.media}); const PendingVideoCompressDialog({super.key, required this.media});
@override @override
State<PendingVideoCompressDialog> createState() => _PendingVideoCompressDialogState(); State<PendingVideoCompressDialog> createState() =>
_PendingVideoCompressDialogState();
} }
class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog> { class _PendingVideoCompressDialogState
extends State<PendingVideoCompressDialog> {
VideoQuality _quality = VideoQuality.DefaultQuality; VideoQuality _quality = VideoQuality.DefaultQuality;
bool _isBusy = false; bool _isBusy = false;
@@ -50,7 +52,7 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
void initState() { void initState() {
super.initState(); super.initState();
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) { _progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
log('[Compress] Progress: $event'); logging.debug('[Paperclip.VideoCompress] Progress: $event');
setState(() { setState(() {
_progress = event / 100; _progress = event / 100;
_isBusy = event < 100; _isBusy = event < 100;
@@ -132,7 +134,9 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
), ),
), ),
const Gap(8), const Gap(8),
Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(), Text('attachmentCompressQualityHint',
style: Theme.of(context).textTheme.bodySmall!)
.tr(),
if (_isBusy) if (_isBusy)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress ?? 0), tween: Tween(begin: 0, end: _progress ?? 0),

View File

@@ -11,6 +11,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_popover.dart'; import 'package:surface/widgets/account/account_popover.dart';
@@ -105,6 +106,22 @@ class ChatMessage extends StatelessWidget {
GestureDetector( GestureDetector(
child: AccountImage( child: AccountImage(
content: user?.avatar, content: user?.avatar,
badge: (user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
)
: null,
), ),
onTap: () { onTap: () {
if (user == null) return; if (user == null) return;

View File

@@ -133,7 +133,8 @@ extension AppPromptExtension on BuildContext {
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
launchUrlString('https://kb.solsynth.dev/solar-network'); launchUrlString(
'https://kb.solsynth.dev/solar-network');
}, },
), ),
], ],
@@ -157,7 +158,17 @@ extension ByteFormatter on int {
if (this == 0) return '0 Bytes'; if (this == 0) return '0 Bytes';
const k = 1024; const k = 1024;
final dm = decimals < 0 ? 0 : decimals; final dm = decimals < 0 ? 0 : decimals;
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(this) / math.log(k)).floor().toInt(); final i = (math.log(this) / math.log(k)).floor().toInt();
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
} }
@@ -167,4 +178,15 @@ extension StringFormatter on String {
String capitalize() { String capitalize() {
return "${this[0].toUpperCase()}${substring(1)}"; return "${this[0].toUpperCase()}${substring(1)}";
} }
String capitalizeEachWord() {
if (isEmpty) {
return this;
}
return split(' ')
.map((word) => word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1)}'
: '')
.join(' ');
}
} }

View File

@@ -1,6 +1,9 @@
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/theme_map.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
@@ -72,21 +75,27 @@ class MarkdownTextContent extends StatelessWidget {
), ),
code: GoogleFonts.robotoMono(height: 1), code: GoogleFonts.robotoMono(height: 1),
), ),
builders: {}, builders: {
'latex': LatexElementBuilder(),
'code': HighlightBuilder(),
},
softLineBreak: true, softLineBreak: true,
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[ <markdown.BlockSyntax>[
markdown.CodeBlockSyntax(),
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
markdown.CodeBlockSyntax(),
markdown.FencedCodeBlockSyntax(),
LatexBlockSyntax(),
], ],
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
if (isAutoWarp) markdown.LineBreakSyntax(), if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(), _UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(context), _CustomEmoteInlineSyntax(context),
markdown.AutolinkSyntax(), markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(), markdown.CodeSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes LatexInlineSyntax(),
], ],
), ),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
@@ -260,3 +269,56 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
return true; return true;
} }
} }
class HighlightBuilder extends MarkdownElementBuilder {
@override
Widget? visitElementAfterWithContext(
BuildContext context,
markdown.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
final isDark = Theme.of(context).brightness == Brightness.dark;
if (element.attributes['class'] == null &&
!element.textContent.trim().contains('\n')) {
return Container(
padding:
EdgeInsets.only(top: 0.0, right: 4.0, bottom: 1.75, left: 4.0),
margin: EdgeInsets.symmetric(horizontal: 2.0),
color: Colors.black12,
child: Text(
element.textContent,
style: GoogleFonts.robotoMono(textStyle: preferredStyle),
));
} else {
var language = 'plaintext';
final pattern = RegExp(r'^language-(.+)$');
if (element.attributes['class'] != null &&
pattern.hasMatch(element.attributes['class'] ?? '')) {
language =
pattern.firstMatch(element.attributes['class'] ?? '')?.group(1) ??
'plaintext';
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: HighlightView(
element.textContent.trim(),
language: language,
theme: {
...(isDark ? themeMap['a11y-dark']! : themeMap['a11y-light']!),
'root': (isDark
? TextStyle(
backgroundColor: Colors.transparent,
color: Color(0xfff8f8f2))
: TextStyle(
backgroundColor: Colors.transparent,
color: Color(0xff545454)))
},
padding: EdgeInsets.all(12),
textStyle: GoogleFonts.robotoMono(textStyle: preferredStyle),
),
);
}
}
}

View File

@@ -193,7 +193,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.49.0) - sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
@@ -378,7 +378,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f

View File

@@ -405,10 +405,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dio_web_adapter name: dio_web_adapter
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -525,10 +525,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "6f6bfa8797f296965bdc3e1f702574ab49a540c19b9237b401e7c2b25dfe594c" sha256: "9467b7c4eedf0bd4c9306b0ec12455b278f6366962be061d0978a446c103c111"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" version: "9.0.1"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -679,7 +679,7 @@ packages:
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: "direct main"
description: description:
name: flutter_cache_manager name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
@@ -710,6 +710,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
flutter_highlight:
dependency: "direct main"
description:
name: flutter_highlight
sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_inappwebview: flutter_inappwebview:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -803,6 +811,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6+2" version: "0.7.6+2"
flutter_markdown_latex:
dependency: "direct main"
description:
name: flutter_markdown_latex
sha256: "839e76a84abb3632ffcebbd450cf93c7e9894af65622527d23f0084cee1bfd04"
url: "https://pub.dev"
source: hosted
version: "0.3.4"
flutter_math_fork:
dependency: transitive
description:
name: flutter_math_fork
sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
flutter_native_splash: flutter_native_splash:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -873,18 +897,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: freezed name: freezed
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" sha256: "532008570b7fd20310db8cb9c8ebc5bafd5aa4e52c4358db4e5ddc29f74f4be3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.8" version: "3.0.1"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" version: "3.0.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -941,6 +965,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
group_button:
dependency: transitive
description:
name: group_button
sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940"
url: "https://pub.dev"
source: hosted
version: "5.3.4"
highlight:
dependency: transitive
description:
name: highlight
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
home_widget: home_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1393,18 +1433,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.1" version: "8.3.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.2.0"
pasteboard: pasteboard:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1950,10 +1990,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233" sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.30" version: "0.5.31"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
@@ -2018,6 +2058,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.3.1"
talker:
dependency: "direct main"
description:
name: talker
sha256: "5ab7d974ad92042b3e2382441c41ec4c6e5b3fa2b4b024d8ccbfc4bc2244b7bb"
url: "https://pub.dev"
source: hosted
version: "4.6.14"
talker_dio_logger:
dependency: "direct main"
description:
name: talker_dio_logger
sha256: "71780c52951d36e94964ca06158d827dfc67aa2fb75c8b880603cfefa4377b39"
url: "https://pub.dev"
source: hosted
version: "4.6.14"
talker_flutter:
dependency: "direct main"
description:
name: talker_flutter
sha256: "0cc816260b226c0ff930909c9f22984316b652b140f5eabb97ae9813ee0de135"
url: "https://pub.dev"
source: hosted
version: "4.6.14"
talker_logger:
dependency: transitive
description:
name: talker_logger
sha256: "16ff0cfdf011f65b37957c9ff7ef7043dd9f1c8af3ccb4a44ac4a448defb9eb5"
url: "https://pub.dev"
source: hosted
version: "4.6.14"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -2050,6 +2122,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.2" version: "0.3.2"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@@ -47,7 +47,7 @@ dependencies:
dio: ^5.7.0 dio: ^5.7.0
dio_smart_retry: ^7.0.1 dio_smart_retry: ^7.0.1
very_good_infinite_list: ^0.9.0 very_good_infinite_list: ^0.9.0
freezed_annotation: ^2.4.4 freezed_annotation: ^3.0.0
json_annotation: ^4.9.0 json_annotation: ^4.9.0
gap: ^3.0.1 gap: ^3.0.1
markdown: ^7.2.2 markdown: ^7.2.2
@@ -128,6 +128,12 @@ dependencies:
drift: ^2.25.1 drift: ^2.25.1
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
local_notifier: ^0.1.6 local_notifier: ^0.1.6
flutter_markdown_latex: ^0.3.4
flutter_highlight: ^0.7.0
talker_flutter: ^4.6.14
talker_dio_logger: ^4.6.14
talker: ^4.6.14
flutter_cache_manager: ^3.4.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -140,7 +146,7 @@ dev_dependencies:
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
build_runner: ^2.4.15 build_runner: ^2.4.15
freezed: ^2.5.7 freezed: ^3.0.1
json_serializable: ^6.8.0 json_serializable: ^6.8.0
icons_launcher: ^3.0.0 icons_launcher: ^3.0.0
flutter_native_splash: ^2.4.2 flutter_native_splash: ^2.4.2