From ae9743c84f2bd996e77be8699b6652f9ecc54507 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 27 Feb 2025 23:30:08 +0800 Subject: [PATCH] :recycle: Refactor logging module --- assets/translations/en-US.json | 3 +- assets/translations/zh-CN.json | 3 +- assets/translations/zh-HK.json | 3 +- assets/translations/zh-TW.json | 3 +- lib/controllers/chat_message_controller.dart | 4 +- lib/logger.dart | 9 +- lib/providers/link_preview.dart | 6 +- lib/providers/notification.dart | 12 +- lib/providers/sn_network.dart | 45 +++-- lib/providers/sn_sticker.dart | 7 +- lib/providers/userinfo.dart | 7 +- lib/providers/websocket.dart | 18 +- lib/screens/logging.dart | 161 +++++++++++++----- .../pending_attachment_compress.dart | 14 +- lib/widgets/dialog.dart | 26 ++- 15 files changed, 230 insertions(+), 91 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 386f7b0..44db9b4 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -730,5 +730,6 @@ "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." + "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." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index e99331a..51130e0 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -728,5 +728,6 @@ "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。", "runtimeLogs": "运行时日志", "runtimeLogsOpen": "打开日志文件", - "runtimeLogsDescription": "显示运行时的日志记录。" + "runtimeLogsDescription": "显示运行时的日志记录。", + "signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 905094f..87f646b 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -728,5 +728,6 @@ "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。", "runtimeLogs": "運行時日誌", "runtimeLogsOpen": "打開日誌文件", - "runtimeLogsDescription": "顯示運行時的日誌記錄。" + "runtimeLogsDescription": "顯示運行時的日誌記錄。", + "signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 3804acb..7ea5150 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -728,5 +728,6 @@ "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。", "runtimeLogs": "運行時日誌", "runtimeLogsOpen": "打開日誌文件", - "runtimeLogsDescription": "顯示運行時的日誌記錄。" + "runtimeLogsDescription": "顯示運行時的日誌記錄。", + "signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。" } diff --git a/lib/controllers/chat_message_controller.dart b/lib/controllers/chat_message_controller.dart index db3fbf3..f38106c 100644 --- a/lib/controllers/chat_message_controller.dart +++ b/lib/controllers/chat_message_controller.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import 'dart:math' as math; import 'package:dio/dio.dart'; @@ -8,6 +7,7 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:surface/database/database.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/database.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; @@ -532,7 +532,7 @@ class ChatMessageController extends ChangeNotifier { }, ).toJson(), )); - log('[Messaging] Send read event request: $_readEventAnchor'); + logging.debug('[Messaging] Send read event request: $_readEventAnchor'); } @override diff --git a/lib/logger.dart b/lib/logger.dart index 7c2cad4..4b36e9d 100644 --- a/lib/logger.dart +++ b/lib/logger.dart @@ -1,3 +1,10 @@ import 'package:talker/talker.dart'; -final logging = Talker(); +final logging = Talker( + settings: TalkerSettings( + enabled: true, + useHistory: true, + maxHistoryItems: 1000, + useConsoleLogs: true, + ), +); diff --git a/lib/providers/link_preview.dart b/lib/providers/link_preview.dart index 4f6b2fe..535611c 100644 --- a/lib/providers/link_preview.dart +++ b/lib/providers/link_preview.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/link.dart'; @@ -20,7 +20,7 @@ class SnLinkPreviewProvider { final target = b64.encode(url); if (_cache.containsKey(target)) return _cache[target]; - log('[LinkPreview] Fetching $url ($target)'); + logging.debug('[LinkPreview] Fetching $url ($target)'); try { final resp = await _sn.client.get('/cgi/re/link/$target'); @@ -28,7 +28,7 @@ class SnLinkPreviewProvider { _cache[url] = meta; return meta; } catch (err) { - log('[LinkPreview] Failed to fetch $url ($target)...'); + logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err); return null; } } diff --git a/lib/providers/notification.dart b/lib/providers/notification.dart index fa003d2..7c6e710 100644 --- a/lib/providers/notification.dart +++ b/lib/providers/notification.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; 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:local_notifier/local_notifier.dart'; import 'package:provider/provider.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; @@ -48,11 +48,13 @@ class NotificationProvider extends ChangeNotifier { var deviceUuid = await FlutterUdid.consistentUdid; 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; } else { - log('Device UUID is $deviceUuid'); - log('Registering device push notifications...'); + logging.info('[Push Notification] Device UUID is $deviceUuid'); + logging + .info('[Push Notification] Registering device push notifications...'); } if (Platform.isIOS || Platform.isMacOS) { @@ -62,7 +64,7 @@ class NotificationProvider extends ChangeNotifier { provider = 'fcm'; token = await FirebaseMessaging.instance.getToken(); } - log('Device Push Token is $token'); + logging.info('[Push Notification] Device Push Token is $token'); await _sn.client.post( '/cgi/id/notifications/subscription', diff --git a/lib/providers/sn_network.dart b/lib/providers/sn_network.dart index bba2a9b..6385ff2 100644 --- a/lib/providers/sn_network.dart +++ b/lib/providers/sn_network.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import 'dart:io'; 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:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/widget.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 = [ ('Solar Network', 'https://api.sn.solsynth.dev'), @@ -36,6 +38,17 @@ class SnNetworkProvider { client = Dio(); + client.interceptors.add( + TalkerDioLogger( + talker: logging, + settings: const TalkerDioLoggerSettings( + printRequestHeaders: false, + printResponseHeaders: false, + printResponseMessage: true, + ), + ), + ); + client.interceptors.add(RetryInterceptor( dio: client, retries: 3, @@ -69,7 +82,6 @@ class SnNetworkProvider { _prefs = _config.prefs; client.options.baseUrl = _config.serverUrl; }); - } static Future createOffContextClient() async { @@ -91,7 +103,8 @@ class SnNetworkProvider { RequestOptions options, RequestInterceptorHandler handler, ) 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(kRtkStoreKey, rtk); }); @@ -103,7 +116,8 @@ class SnNetworkProvider { }, ), ); - client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; + client.options.baseUrl = + prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; return client; } @@ -119,7 +133,8 @@ class SnNetworkProvider { platformInfo = 'Web; ${deviceInfo.vendor}'; } else if (Platform.isAndroid) { 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) { final deviceInfo = await DeviceInfoPlugin().iosInfo; platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}'; @@ -128,7 +143,8 @@ class SnNetworkProvider { platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}'; } else if (Platform.isWindows) { final deviceInfo = await DeviceInfoPlugin().windowsInfo; - platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; + platformInfo = + 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; } else if (Platform.isLinux) { final deviceInfo = await DeviceInfoPlugin().linuxInfo; platformInfo = 'Linux; ${deviceInfo.prettyName}'; @@ -148,12 +164,15 @@ class SnNetworkProvider { final tkLock = Lock(); Future 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); }); } - static Future _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async { + static Future _getFreshAtk(Dio client, String? atk, String? rtk, + Function(String atk, String rtk)? onRefresh) async { if (_refreshCompleter != null) { return await _refreshCompleter!.future; } else { @@ -185,7 +204,8 @@ class SnNetworkProvider { final payload = b64.decode(rawPayload); final exp = jsonDecode(payload)['exp']; 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); if (result == null) { atk = null; @@ -199,12 +219,12 @@ class SnNetworkProvider { _refreshCompleter!.complete(atk); return atk; } else { - log('Access token refresh failed...'); + logging.error('[Auth] Access token refresh failed...'); _refreshCompleter!.complete(null); } } } catch (err) { - log('Failed to authenticate user: $err'); + logging.error('[Auth] Failed to authenticate user...', err); _refreshCompleter!.completeError(err); } finally { _refreshCompleter = null; @@ -237,7 +257,8 @@ class SnNetworkProvider { 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; final dio = Dio(); diff --git a/lib/providers/sn_sticker.dart b/lib/providers/sn_sticker.dart index 0529b3f..4a4cc78 100644 --- a/lib/providers/sn_sticker.dart +++ b/lib/providers/sn_sticker.dart @@ -1,7 +1,6 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; @@ -51,7 +50,7 @@ class SnStickerProvider { return sticker; } catch (err) { _cache[alias] = null; - log('[Sticker] Failed to lookup sticker $alias: $err'); + logging.warning('[Sticker] Failed to lookup sticker $alias', err); } return null; @@ -66,7 +65,7 @@ class SnStickerProvider { _cacheSticker(sticker); } } catch (err) { - log('[Sticker] Failed to list stickers: $err'); + logging.error('[Sticker] Failed to list stickers...', err); rethrow; } } diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart index 081bed0..15e966e 100644 --- a/lib/providers/userinfo.dart +++ b/lib/providers/userinfo.dart @@ -1,8 +1,7 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/account.dart'; @@ -30,8 +29,8 @@ class UserProvider extends ChangeNotifier { notifyListeners(); refreshUser().then((value) async { if (value != null) { - log('Logged in as @${value.name}'); - log('Atk: ${await atk}'); + logging.info('[Auth] Logged in as @${value.name}'); + logging.debug('[Auth] Access token: ${await atk}'); } }); } diff --git a/lib/providers/websocket.dart b/lib/providers/websocket.dart index 69418da..fe8ea24 100644 --- a/lib/providers/websocket.dart +++ b/lib/providers/websocket.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:surface/logger.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/websocket.dart'; @@ -30,7 +30,7 @@ class WebSocketProvider extends ChangeNotifier { if (isConnected) return; if (!_ua.isAuthorized) return; - log('[WebSocket] Connecting to the server...'); + logging.debug('[WebSocket] Connecting to the server...'); await connect(); } @@ -62,17 +62,14 @@ class WebSocketProvider extends ChangeNotifier { await conn!.ready; _wsStream = conn!.stream.asBroadcastStream(); listen(); - log('[WebSocket] Connected to server!'); + logging.info('[WebSocket] Connected to server!'); isConnected = true; } catch (err) { - if (err is WebSocketChannelException) { - log('Failed to connect to websocket: ${(err.inner as dynamic).message}'); - } else { - log('Failed to connect to websocket: $err'); - } + logging.error('[WebSocket] Failed to connect to websocket...', err); if (!noRetry) { - log('Retry connecting to websocket in 3 seconds...'); + logging.warning( + '[WebSocket] Retry connecting to websocket in 3 seconds...'); return Future.delayed( const Duration(seconds: 3), () => connect(noRetry: true), @@ -100,7 +97,8 @@ class WebSocketProvider extends ChangeNotifier { _wsStream!.listen( (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); }, onDone: () { diff --git a/lib/screens/logging.dart b/lib/screens/logging.dart index 263951e..0a1ac55 100644 --- a/lib/screens/logging.dart +++ b/lib/screens/logging.dart @@ -1,11 +1,14 @@ 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/talker.dart'; +import 'package:talker_dio_logger/dio_logs.dart'; +import 'package:talker_flutter/talker_flutter.dart'; final Map kLogLevelIcons = { LogLevel.error: Symbols.error, @@ -16,15 +19,6 @@ final Map kLogLevelIcons = { LogLevel.verbose: Symbols.info_i, }; -final Map 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 kLogLevelFilled = { LogLevel.error: false, LogLevel.critical: true, @@ -39,43 +33,132 @@ class DebugLoggingScreen extends StatelessWidget { @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: SelectionArea( - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: logging.history.length, - itemBuilder: (context, index) { - final log = logging.history[index]; - return ListTile( - leading: Icon( - kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help, - color: kLogLevelColors[log.logLevel ?? LogLevel.debug], - fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false) - ? 1 - : 0, - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + 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), + ), + 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( - log.message ?? 'unknown'.tr(), + log.displayMessage, style: GoogleFonts.robotoMono(fontSize: 13), ), - if (log.error != null) - Text( - log.error!.toString(), - style: GoogleFonts.robotoMono(fontSize: 13), - ).bold(), - ], - ), - subtitle: Text(log.time.toString()).fontSize(11), - ); - }, - ), + 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.time}]', log.message, log.error?.toString()] + .where((ele) => ele != null) + .join('\n'), + ), + ); + }, + ); + }, ), ); } diff --git a/lib/widgets/attachment/pending_attachment_compress.dart b/lib/widgets/attachment/pending_attachment_compress.dart index 6568922..3c67ed1 100644 --- a/lib/widgets/attachment/pending_attachment_compress.dart +++ b/lib/widgets/attachment/pending_attachment_compress.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:cross_file/cross_file.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:styled_widget/styled_widget.dart'; import 'package:surface/controllers/post_write_controller.dart'; +import 'package:surface/logger.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:video_compress/video_compress.dart'; @@ -17,10 +17,12 @@ class PendingVideoCompressDialog extends StatefulWidget { const PendingVideoCompressDialog({super.key, required this.media}); @override - State createState() => _PendingVideoCompressDialogState(); + State createState() => + _PendingVideoCompressDialogState(); } -class _PendingVideoCompressDialogState extends State { +class _PendingVideoCompressDialogState + extends State { VideoQuality _quality = VideoQuality.DefaultQuality; bool _isBusy = false; @@ -50,7 +52,7 @@ class _PendingVideoCompressDialogState extends State void initState() { super.initState(); _progressSubscription = VideoCompress.compressProgress$.subscribe((event) { - log('[Compress] Progress: $event'); + logging.debug('[Paperclip.VideoCompress] Progress: $event'); setState(() { _progress = event / 100; _isBusy = event < 100; @@ -132,7 +134,9 @@ class _PendingVideoCompressDialogState extends State ), ), const Gap(8), - Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(), + Text('attachmentCompressQualityHint', + style: Theme.of(context).textTheme.bodySmall!) + .tr(), if (_isBusy) TweenAnimationBuilder( tween: Tween(begin: 0, end: _progress ?? 0), diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 537f947..61a4cdc 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -133,7 +133,8 @@ extension AppPromptExtension on BuildContext { ), recognizer: TapGestureRecognizer() ..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'; const k = 1024; 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(); return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; } @@ -167,4 +178,15 @@ extension StringFormatter on String { String capitalize() { 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(' '); + } }