mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-03-31 17:10:29 +02:00
- Cache SharedPreferences.getInstance() in providers (settings, theme, recent_access) - Pre-compute download counts in queue provider to avoid repeated filtering - Add identical() caching for RecentAccessView in HomeTab - Use selective watching for exploreProvider (sections, greeting, isLoading only) - Move isYTMusicQuickPicks computation to ExploreSection.fromJson() - Hoist static RegExp patterns to avoid repeated compilation - Use batch operations for iOS path migration in history_database - Replace containsKey+lookup with single lookup in palette_service - Pre-compute lowercase strings outside filter loops in logger - Fix _isLoaded race condition in DownloadHistoryNotifier
305 lines
7.8 KiB
Dart
305 lines
7.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:logger/logger.dart';
|
|
import 'package:spotiflac_android/services/platform_bridge.dart';
|
|
|
|
class LogEntry {
|
|
final DateTime timestamp;
|
|
final String level;
|
|
final String tag;
|
|
final String message;
|
|
final String? error;
|
|
final bool isFromGo; // Track if this log came from Go backend
|
|
|
|
LogEntry({
|
|
required this.timestamp,
|
|
required this.level,
|
|
required this.tag,
|
|
required this.message,
|
|
this.error,
|
|
this.isFromGo = false,
|
|
});
|
|
|
|
String get formattedTime {
|
|
final h = timestamp.hour.toString().padLeft(2, '0');
|
|
final m = timestamp.minute.toString().padLeft(2, '0');
|
|
final s = timestamp.second.toString().padLeft(2, '0');
|
|
final ms = timestamp.millisecond.toString().padLeft(3, '0');
|
|
return '$h:$m:$s.$ms';
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
final errorPart = error != null ? ' | $error' : '';
|
|
final goPart = isFromGo ? ' [Go]' : '';
|
|
return '[$formattedTime] [$level]$goPart [$tag] $message$errorPart';
|
|
}
|
|
}
|
|
|
|
class LogBuffer extends ChangeNotifier {
|
|
static final LogBuffer _instance = LogBuffer._internal();
|
|
factory LogBuffer() => _instance;
|
|
LogBuffer._internal();
|
|
|
|
static const int maxEntries = 500;
|
|
final Queue<LogEntry> _entries = Queue<LogEntry>();
|
|
Timer? _goLogTimer;
|
|
int _lastGoLogIndex = 0;
|
|
|
|
/// Whether logging is enabled (controlled by settings)
|
|
/// User must enable "Detailed Logging" in settings to capture logs
|
|
static bool _loggingEnabled = false;
|
|
static bool get loggingEnabled => _loggingEnabled;
|
|
static set loggingEnabled(bool value) {
|
|
_loggingEnabled = value;
|
|
if (value) {
|
|
PlatformBridge.setGoLoggingEnabled(true).catchError((_) {});
|
|
} else {
|
|
PlatformBridge.setGoLoggingEnabled(false).catchError((_) {});
|
|
}
|
|
}
|
|
|
|
List<LogEntry> get entries => _entries.toList();
|
|
int get length => _entries.length;
|
|
|
|
void add(LogEntry entry) {
|
|
// Skip adding if logging is disabled (except for errors which are always logged)
|
|
if (!_loggingEnabled && entry.level != 'ERROR' && entry.level != 'FATAL') {
|
|
return;
|
|
}
|
|
|
|
if (_entries.length >= maxEntries) {
|
|
_entries.removeFirst();
|
|
}
|
|
_entries.add(entry);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Start polling Go backend logs
|
|
void startGoLogPolling() {
|
|
_goLogTimer?.cancel();
|
|
_goLogTimer = Timer.periodic(const Duration(milliseconds: 500), (_) async {
|
|
await _fetchGoLogs();
|
|
});
|
|
}
|
|
|
|
/// Stop polling Go backend logs
|
|
void stopGoLogPolling() {
|
|
_goLogTimer?.cancel();
|
|
_goLogTimer = null;
|
|
}
|
|
|
|
/// Fetch logs from Go backend since last index
|
|
Future<void> _fetchGoLogs() async {
|
|
try {
|
|
final result = await PlatformBridge.getGoLogsSince(_lastGoLogIndex);
|
|
final logs = result['logs'] as List<dynamic>? ?? [];
|
|
final nextIndex = result['next_index'] as int? ?? _lastGoLogIndex;
|
|
|
|
for (final log in logs) {
|
|
final timestamp = log['timestamp'] as String? ?? '';
|
|
final level = log['level'] as String? ?? 'INFO';
|
|
final tag = log['tag'] as String? ?? 'Go';
|
|
final message = log['message'] as String? ?? '';
|
|
|
|
// Parse timestamp (format: "15:04:05.000")
|
|
DateTime parsedTime = DateTime.now();
|
|
if (timestamp.isNotEmpty) {
|
|
try {
|
|
final parts = timestamp.split(':');
|
|
if (parts.length >= 3) {
|
|
final secParts = parts[2].split('.');
|
|
parsedTime = DateTime(
|
|
parsedTime.year, parsedTime.month, parsedTime.day,
|
|
int.parse(parts[0]), int.parse(parts[1]),
|
|
int.parse(secParts[0]),
|
|
secParts.length > 1 ? int.parse(secParts[1]) : 0,
|
|
);
|
|
}
|
|
} catch (_) {
|
|
}
|
|
}
|
|
|
|
add(LogEntry(
|
|
timestamp: parsedTime,
|
|
level: level,
|
|
tag: tag,
|
|
message: message,
|
|
isFromGo: true,
|
|
));
|
|
}
|
|
|
|
_lastGoLogIndex = nextIndex;
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
debugPrint('Failed to fetch Go logs: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
void clear() {
|
|
_entries.clear();
|
|
_lastGoLogIndex = 0;
|
|
PlatformBridge.clearGoLogs().catchError((_) {});
|
|
notifyListeners();
|
|
}
|
|
|
|
String export() {
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('SpotiFLAC Log Export');
|
|
buffer.writeln('Generated: ${DateTime.now().toIso8601String()}');
|
|
buffer.writeln('Entries: ${_entries.length}');
|
|
buffer.writeln('=' * 60);
|
|
buffer.writeln();
|
|
for (final entry in _entries) {
|
|
buffer.writeln(entry.toString());
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
List<LogEntry> filter({String? level, String? tag, String? search}) {
|
|
final tagLower = tag?.toLowerCase();
|
|
final searchLower = search?.toLowerCase();
|
|
|
|
return _entries.where((entry) {
|
|
if (level != null && level != 'ALL' && entry.level != level) {
|
|
return false;
|
|
}
|
|
if (tagLower != null && !entry.tag.toLowerCase().contains(tagLower)) {
|
|
return false;
|
|
}
|
|
if (searchLower != null && searchLower.isNotEmpty) {
|
|
return entry.message.toLowerCase().contains(searchLower) ||
|
|
entry.tag.toLowerCase().contains(searchLower) ||
|
|
(entry.error?.toLowerCase().contains(searchLower) ?? false);
|
|
}
|
|
return true;
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
class BufferedOutput extends LogOutput {
|
|
final String tag;
|
|
|
|
BufferedOutput(this.tag);
|
|
|
|
@override
|
|
void output(OutputEvent event) {
|
|
if (kDebugMode) {
|
|
for (final line in event.lines) {
|
|
debugPrint(line);
|
|
}
|
|
}
|
|
|
|
final level = _levelToString(event.level);
|
|
final message = event.lines.join('\n');
|
|
|
|
LogBuffer().add(LogEntry(
|
|
timestamp: DateTime.now(),
|
|
level: level,
|
|
tag: tag,
|
|
message: message,
|
|
));
|
|
}
|
|
|
|
String _levelToString(Level level) {
|
|
switch (level) {
|
|
case Level.debug:
|
|
return 'DEBUG';
|
|
case Level.info:
|
|
return 'INFO';
|
|
case Level.warning:
|
|
return 'WARN';
|
|
case Level.error:
|
|
return 'ERROR';
|
|
case Level.fatal:
|
|
return 'FATAL';
|
|
default:
|
|
return 'LOG';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Global logger instance for the app
|
|
final log = Logger(
|
|
printer: PrettyPrinter(
|
|
methodCount: 0,
|
|
errorMethodCount: 5,
|
|
lineLength: 80,
|
|
colors: true,
|
|
printEmojis: false,
|
|
dateTimeFormat: DateTimeFormat.none,
|
|
),
|
|
level: Level.debug,
|
|
);
|
|
|
|
class AppLogger {
|
|
final String _tag;
|
|
late final Logger? _logger;
|
|
|
|
AppLogger(this._tag) {
|
|
if (kDebugMode) {
|
|
_logger = Logger(
|
|
printer: SimplePrinter(printTime: false, colors: false),
|
|
output: BufferedOutput(_tag),
|
|
level: Level.debug,
|
|
);
|
|
} else {
|
|
_logger = null;
|
|
}
|
|
}
|
|
|
|
void _addToBuffer(String level, String message, {String? error}) {
|
|
LogBuffer().add(LogEntry(
|
|
timestamp: DateTime.now(),
|
|
level: level,
|
|
tag: _tag,
|
|
message: message,
|
|
error: error,
|
|
));
|
|
}
|
|
|
|
void d(String message) {
|
|
if (kDebugMode) {
|
|
_logger?.d(message);
|
|
} else {
|
|
_addToBuffer('DEBUG', message);
|
|
}
|
|
}
|
|
|
|
void i(String message) {
|
|
if (kDebugMode) {
|
|
_logger?.i(message);
|
|
} else {
|
|
_addToBuffer('INFO', message);
|
|
}
|
|
}
|
|
|
|
void w(String message) {
|
|
if (kDebugMode) {
|
|
_logger?.w(message);
|
|
} else {
|
|
_addToBuffer('WARN', message);
|
|
}
|
|
}
|
|
|
|
void e(String message, [Object? error, StackTrace? stackTrace]) {
|
|
if (error != null) {
|
|
_addToBuffer('ERROR', message, error: error.toString());
|
|
if (kDebugMode) {
|
|
debugPrint('[$_tag] ERROR: $message | $error');
|
|
if (stackTrace != null) {
|
|
debugPrint(stackTrace.toString());
|
|
}
|
|
}
|
|
} else {
|
|
if (kDebugMode) {
|
|
_logger?.e(message);
|
|
} else {
|
|
_addToBuffer('ERROR', message);
|
|
}
|
|
}
|
|
}
|
|
}
|