♻️ Refactor logging module

This commit is contained in:
LittleSheep 2025-02-27 23:30:08 +08:00
parent 32bf834108
commit ae9743c84f
15 changed files with 230 additions and 91 deletions

View File

@ -730,5 +730,6 @@
"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", "debugLogging": "Runtime Logs",
"runtimeLogsOpen": "Open Logs", "runtimeLogsOpen": "Open Logs",
"runtimeLogsDescription": "Show the runtime logs to help debugging." "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."
} }

View File

@ -728,5 +728,6 @@
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。", "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "运行时日志", "runtimeLogs": "运行时日志",
"runtimeLogsOpen": "打开日志文件", "runtimeLogsOpen": "打开日志文件",
"runtimeLogsDescription": "显示运行时的日志记录。" "runtimeLogsDescription": "显示运行时的日志记录。",
"signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。"
} }

View File

@ -728,5 +728,6 @@
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。", "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌", "runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件", "runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。" "runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。"
} }

View File

@ -728,5 +728,6 @@
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。", "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌", "runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件", "runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。" "runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。"
} }

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

View File

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

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

@ -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

@ -1,11 +1,14 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/logger.dart'; import 'package:surface/logger.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:talker/talker.dart'; import 'package:talker_dio_logger/dio_logs.dart';
import 'package:talker_flutter/talker_flutter.dart';
final Map<LogLevel, IconData> kLogLevelIcons = { final Map<LogLevel, IconData> kLogLevelIcons = {
LogLevel.error: Symbols.error, LogLevel.error: Symbols.error,
@ -16,15 +19,6 @@ final Map<LogLevel, IconData> kLogLevelIcons = {
LogLevel.verbose: Symbols.info_i, LogLevel.verbose: Symbols.info_i,
}; };
final Map<LogLevel, Color> kLogLevelColors = {
LogLevel.error: Colors.red,
LogLevel.critical: Colors.red,
LogLevel.warning: Colors.orange,
LogLevel.info: Colors.blue,
LogLevel.debug: Colors.green,
LogLevel.verbose: Colors.green,
};
final Map<LogLevel, bool> kLogLevelFilled = { final Map<LogLevel, bool> kLogLevelFilled = {
LogLevel.error: false, LogLevel.error: false,
LogLevel.critical: true, LogLevel.critical: true,
@ -39,43 +33,132 @@ class DebugLoggingScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final talkerTheme = TalkerScreenTheme.fromTheme(Theme.of(context));
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('debugLogging').tr(), title: Text('debugLogging').tr(),
actions: [
IconButton(
onPressed: () {
logging.cleanHistory();
Navigator.pop(context);
},
icon: const Icon(Symbols.delete),
),
],
), ),
body: SelectionArea( body: ListView.builder(
child: ListView.builder( reverse: true,
padding: EdgeInsets.zero, padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
itemCount: logging.history.length, itemCount: logging.history.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final log = logging.history[index]; final log = logging.history[index];
return ListTile( final color = log.getFlutterColor(talkerTheme);
leading: Icon( return ListTile(
kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help, minTileHeight: 0,
color: kLogLevelColors[log.logLevel ?? LogLevel.debug], tileColor: color.withOpacity(0.2),
fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false) leading: Icon(
? 1 kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help,
: 0, color: color,
), fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false)
title: Column( ? 1
crossAxisAlignment: CrossAxisAlignment.start, : 0,
children: [ ),
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),
),
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),
),
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( Text(
log.message ?? 'unknown'.tr(), log.displayMessage,
style: GoogleFonts.robotoMono(fontSize: 13), style: GoogleFonts.robotoMono(fontSize: 13),
), ),
if (log.error != null) if (log.exception != null)
Text( Text(
log.error!.toString(), log.displayException,
style: GoogleFonts.robotoMono(fontSize: 13), style: GoogleFonts.robotoMono(fontSize: 13),
).bold(), ).bold(),
], if (log.error != null)
), Text(
subtitle: Text(log.time.toString()).fontSize(11), 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.time}]', log.message, log.error?.toString()]
.where((ele) => ele != null)
.join('\n'),
),
);
},
);
},
), ),
); );
} }

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

@ -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(' ');
}
} }