fix: improve logging for release builds and UI improvements

- Fix Flutter logs not appearing in release mode by bypassing Logger package
- Add detailed logging for Deezer search API calls
- Replace music_note icon with app logo on home screen
- Remove shadow/border from logo in About and Home screens
- Align icon size (40x40) with avatar in About page for consistent layout
This commit is contained in:
zarzet
2026-01-11 02:27:26 +07:00
parent 11e7034cec
commit f2aca734a3
8 changed files with 457 additions and 153 deletions
+28 -19
View File
@@ -1,6 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('TrackProvider');
class TrackState {
final List<Track> tracks;
@@ -210,54 +213,60 @@ class TrackNotifier extends Notifier<TrackState> {
// Use Deezer or Spotify based on settings
final source = metadataSource ?? 'deezer';
// Debug log to show which source is being used
// ignore: avoid_print
print('[Search] Using metadata source: $source for query: "$query"');
_log.i('Search started: source=$source, query="$query"');
Map<String, dynamic> results;
if (source == 'deezer') {
_log.d('Calling Deezer search API...');
results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 5);
// ignore: avoid_print
print('[Search] Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks');
_log.i('Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists');
} else {
_log.d('Calling Spotify search API...');
results = await PlatformBridge.searchSpotifyAll(query, trackLimit: 20, artistLimit: 5);
// ignore: avoid_print
print('[Search] Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks');
_log.i('Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists');
}
if (!_isRequestValid(requestId)) return; // Request cancelled
if (!_isRequestValid(requestId)) {
_log.w('Search request cancelled (requestId=$requestId)');
return;
}
final trackList = results['tracks'] as List<dynamic>? ?? [];
final artistList = results['artists'] as List<dynamic>? ?? [];
_log.d('Raw results: ${trackList.length} tracks, ${artistList.length} artists');
// Parse tracks with error handling per item
final tracks = <Track>[];
for (final t in trackList) {
for (int i = 0; i < trackList.length; i++) {
final t = trackList[i];
try {
if (t is Map<String, dynamic>) {
tracks.add(_parseSearchTrack(t));
} else {
_log.w('Track[$i] is not a Map: ${t.runtimeType}');
}
} catch (e) {
// ignore: avoid_print
print('[Search] Failed to parse track: $e');
_log.e('Failed to parse track[$i]: $e', e);
}
}
// Parse artists with error handling per item
final artists = <SearchArtist>[];
for (final a in artistList) {
for (int i = 0; i < artistList.length; i++) {
final a = artistList[i];
try {
if (a is Map<String, dynamic>) {
artists.add(_parseSearchArtist(a));
} else {
_log.w('Artist[$i] is not a Map: ${a.runtimeType}');
}
} catch (e) {
// ignore: avoid_print
print('[Search] Failed to parse artist: $e');
_log.e('Failed to parse artist[$i]: $e', e);
}
}
// ignore: avoid_print
print('[Search] Parsed ${tracks.length} tracks, ${artists.length} artists');
_log.i('Search complete: ${tracks.length} tracks, ${artists.length} artists parsed successfully');
state = TrackState(
tracks: tracks,
@@ -265,9 +274,9 @@ class TrackNotifier extends Notifier<TrackState> {
isLoading: false,
hasSearchText: state.hasSearchText,
);
} catch (e) {
if (!_isRequestValid(requestId)) return; // Request cancelled
// Preserve hasSearchText on error so user stays on search screen
} catch (e, stackTrace) {
if (!_isRequestValid(requestId)) return;
_log.e('Search failed: $e', e, stackTrace);
state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText);
}
}
+20 -6
View File
@@ -328,13 +328,27 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
: Column(
children: [
SizedBox(height: screenHeight * 0.06),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
shape: BoxShape.circle,
ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/logo.png',
width: 96,
height: 96,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(24),
),
child: Icon(
Icons.music_note,
size: 48,
color: colorScheme.onPrimaryContainer,
),
),
),
child: Icon(Icons.music_note, size: 48, color: colorScheme.primary),
),
const SizedBox(height: 16),
Text(
+93 -21
View File
@@ -109,14 +109,14 @@ class AboutPage extends StatelessWidget {
githubUsername: 'sachinsenal0x64',
showDivider: true,
),
SettingsItem(
_AboutSettingsItem(
icon: Icons.cloud_outlined,
title: 'DoubleDouble',
subtitle: 'Amazing API for Amazon Music downloads. Thank you for making it free!',
onTap: () => _launchUrl('https://doubledouble.top'),
showDivider: true,
),
SettingsItem(
_AboutSettingsItem(
icon: Icons.music_note_outlined,
title: 'DAB Music',
subtitle: 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!',
@@ -250,26 +250,21 @@ class _AppHeaderCard extends StatelessWidget {
child: Column(
children: [
// App logo
Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.2),
blurRadius: 16,
offset: const Offset(0, 4),
ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/logo.png',
width: 88,
height: 88,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(24),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.cover,
errorBuilder: (_, _, _) => Icon(
child: Icon(
Icons.music_note,
size: 48,
color: colorScheme.onPrimaryContainer,
@@ -417,3 +412,80 @@ class _ContributorItem extends StatelessWidget {
await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
}
/// Settings item with 40x40 icon area to align with contributor avatars
class _AboutSettingsItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final VoidCallback? onTap;
final bool showDivider;
const _AboutSettingsItem({
required this.icon,
required this.title,
this.subtitle,
this.onTap,
this.showDivider = true,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: onTap,
splashColor: colorScheme.primary.withValues(alpha: 0.12),
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
children: [
// Icon with 40x40 size to match avatar
SizedBox(
width: 40,
height: 40,
child: Icon(icon, color: colorScheme.onSurfaceVariant, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodyLarge,
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
],
),
),
if (onTap != null)
Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant),
],
),
),
),
if (showDivider)
Divider(
height: 1,
thickness: 1,
indent: 76, // 20 + 40 + 16 = 76 (same as contributor item)
endIndent: 20,
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
),
],
);
}
}
+46 -17
View File
@@ -50,6 +50,7 @@ class LogBuffer extends ChangeNotifier {
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) {
@@ -242,39 +243,63 @@ final log = Logger(
/// Logger with class/tag prefix for better traceability
/// Now also writes to LogBuffer for in-app viewing
/// Works in both debug and release mode
class AppLogger {
final String _tag;
late final Logger _logger;
late final Logger? _logger;
AppLogger(this._tag) {
_logger = Logger(
printer: SimplePrinter(printTime: false, colors: false),
output: BufferedOutput(_tag),
level: Level.debug,
);
// Only create Logger instance in debug mode
// In release mode, we write directly to LogBuffer
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) {
_logger.d(message);
if (kDebugMode) {
_logger?.d(message);
} else {
// In release mode, write directly to buffer
_addToBuffer('DEBUG', message);
}
}
void i(String message) {
_logger.i(message);
if (kDebugMode) {
_logger?.i(message);
} else {
_addToBuffer('INFO', message);
}
}
void w(String message) {
_logger.w(message);
if (kDebugMode) {
_logger?.w(message);
} else {
_addToBuffer('WARN', message);
}
}
void e(String message, [Object? error, StackTrace? stackTrace]) {
if (error != null) {
LogBuffer().add(LogEntry(
timestamp: DateTime.now(),
level: 'ERROR',
tag: _tag,
message: message,
error: error.toString(),
));
_addToBuffer('ERROR', message, error: error.toString());
if (kDebugMode) {
debugPrint('[$_tag] ERROR: $message | $error');
if (stackTrace != null) {
@@ -282,7 +307,11 @@ class AppLogger {
}
}
} else {
_logger.e(message);
if (kDebugMode) {
_logger?.e(message);
} else {
_addToBuffer('ERROR', message);
}
}
}
}