curl_logger_interceptor 0.0.3
curl_logger_interceptor: ^0.0.3 copied to clipboard
A Dio interceptor that generates curl commands from HTTP requests, including support for FormData and multipart files.
/// first create api_log.dart file and add this
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class ApiLog {
final String method;
final String url;
final Map<String, dynamic>? headers;
final dynamic requestBody;
final int? statusCode;
final dynamic responseBody;
final DateTime timestamp;
final String? errorMessage;
final Duration? duration;
final String? curl;
ApiLog({
required this.method,
required this.url,
this.headers,
this.requestBody,
this.statusCode,
this.responseBody,
required this.timestamp,
this.errorMessage,
this.duration,
this.curl,
});
bool get isSuccess =>
statusCode != null && statusCode! >= 200 && statusCode! < 300;
bool get isError =>
statusCode == null || statusCode! >= 400 || errorMessage != null;
}
class ApiLogger {
static final ApiLogger _instance = ApiLogger._internal();
factory ApiLogger() => _instance;
ApiLogger._internal();
static ApiLogger get instance => _instance;
final List<ApiLog> _logs = [];
final int maxLogs = 100;
List<ApiLog> get logs => List.unmodifiable(_logs);
void addLog(ApiLog log) {
_logs.insert(0, log);
if (_logs.length > maxLogs) {
_logs.removeRange(maxLogs, _logs.length);
}
}
void clearLogs() {
_logs.clear();
}
}
class DraggableApiLoggerButton extends StatefulWidget {
final Widget child;
final bool enabled;
final Color? buttonColor;
final Color? iconColor;
final double size;
const DraggableApiLoggerButton({
super.key,
required this.child,
this.enabled = kDebugMode,
this.buttonColor,
this.iconColor,
this.size = 56.0,
});
@override
State<DraggableApiLoggerButton> createState() =>
_DraggableApiLoggerButtonState();
}
class _DraggableApiLoggerButtonState extends State<DraggableApiLoggerButton> {
Offset _offset = const Offset(20, 100);
bool _isDragging = false;
@override
Widget build(BuildContext context) {
if (!widget.enabled) {
return widget.child;
}
return Stack(
children: [
widget.child,
Positioned(
left: _offset.dx,
top: _offset.dy,
child: GestureDetector(
onPanStart: (details) {
_isDragging = true;
},
onPanUpdate: (details) {
if (_isDragging) {
setState(() {
_offset = Offset(
(_offset.dx + details.delta.dx).clamp(
0.0,
MediaQuery.of(context).size.width - widget.size,
),
(_offset.dy + details.delta.dy).clamp(
0.0,
MediaQuery.of(context).size.height - widget.size - 100,
),
);
});
}
},
onPanEnd: (details) {
_isDragging = false;
},
onTap: () {
if (!_isDragging) {
_showApiLogsDialog(context);
}
},
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: widget.buttonColor ?? Theme.of(context).primaryColor,
shape: BoxShape.circle,
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Icon(
Icons.bug_report_outlined,
color: widget.iconColor ?? Colors.white,
size: widget.size * 0.5,
),
),
),
),
],
);
}
void _showApiLogsDialog(BuildContext context) {
showDialog(context: context, builder: (context) => const ApiLogsDialog());
}
}
class ApiLogsDialog extends StatefulWidget {
const ApiLogsDialog({super.key});
@override
_ApiLogsDialogState createState() => _ApiLogsDialogState();
}
class _ApiLogsDialogState extends State<ApiLogsDialog> {
final String _searchQuery = '';
String _selectedFilter = 'All';
final List<String> _filterOptions = [
'All',
'Success',
'Error',
'GET',
'POST',
'PUT',
'DELETE',
];
@override
Widget build(BuildContext context) {
final filteredLogs = _getFilteredLogs();
return Dialog(
insetPadding: const EdgeInsets.all(10),
child: SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height * 0.9,
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
child: Row(
children: [
const Icon(Icons.bug_report, color: Colors.white),
const SizedBox(width: 8),
Text(
'API Logs (${filteredLogs.length})',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () {
ApiLogger.instance.clearLogs();
setState(() {});
},
icon: const Icon(Icons.clear_all, color: Colors.white),
tooltip: 'Clear Logs',
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
),
],
),
),
// Search and Filter
Container(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 36,
child: TextField(
onTapOutside: (e) => FocusScope.of(context).unfocus(),
decoration: InputDecoration(
hintText: 'Search URLs...',
prefixIcon: const Icon(Icons.search, size: 18),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
),
),
const SizedBox(width: 8),
DropdownButton<String>(
value: _selectedFilter,
items: _filterOptions.map((filter) {
return DropdownMenuItem(
value: filter,
child: Text(filter),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedFilter = value!;
});
},
),
],
),
),
// Logs List
Expanded(
child: filteredLogs.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No API logs found',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: filteredLogs.length,
itemBuilder: (context, index) {
final log = filteredLogs[index];
return ApiLogTile(log: log);
},
),
),
],
),
),
);
}
List<ApiLog> _getFilteredLogs() {
var logs = ApiLogger.instance.logs;
// Apply search filter
if (_searchQuery.isNotEmpty) {
logs = logs
.where(
(log) =>
log.url.toLowerCase().contains(_searchQuery.toLowerCase()) ||
log.method.toLowerCase().contains(_searchQuery.toLowerCase()),
)
.toList();
}
// Apply status filter
switch (_selectedFilter) {
case 'Success':
logs = logs.where((log) => log.isSuccess).toList();
case 'Error':
logs = logs.where((log) => log.isError).toList();
case 'GET':
case 'POST':
case 'PUT':
case 'DELETE':
logs = logs
.where((log) => log.method.toUpperCase() == _selectedFilter)
.toList();
}
return logs;
}
}
class ApiLogTile extends StatelessWidget {
final ApiLog log;
const ApiLogTile({super.key, required this.log});
@override
Widget build(BuildContext context) {
final statusColor = _getStatusColor();
final timeAgo = _getTimeAgo();
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ExpansionTile(
collapsedIconColor: Colors.lightBlue,
iconColor: Colors.green,
leading: Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle),
),
title: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getMethodColor(),
borderRadius: BorderRadius.circular(4),
),
child: Text(
log.method,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
log.url,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: Row(
children: [
if (log.statusCode != null)
Text(
'${log.statusCode}',
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
if (log.duration != null) ...[
const SizedBox(width: 8),
Text('${log.duration!.inMilliseconds}ms'),
],
const Spacer(),
Text(
timeAgo,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSection('Api url', log.url ),
const SizedBox(height: 12),
if (log.headers != null && log.headers!.isNotEmpty) ...[
_buildSection('Headers', log.headers ),
const SizedBox(height: 12),
],
if (log.curl != null) ...[
_buildSection('Curl', log.curl ),
const SizedBox(height: 12),
],
if (log.requestBody != null) ...[
_buildSection('Request Body', log.requestBody ),
const SizedBox(height: 12),
],
if (log.responseBody != null) ...[
_buildSection('Response Body', log.responseBody ),
const SizedBox(height: 12),
],
if (log.errorMessage != null) ...[
_buildSection('Error', log.errorMessage ),
],
],
),
),
],
),
);
}
Widget _buildSection(String title, dynamic content) {
String contentStr;
if (content is Map || content is List) {
// Pretty print JSON with 2 spaces indentation
const encoder = JsonEncoder.withIndent(' ');
contentStr = encoder.convert(content);
} else if (content is String) {
// Try to detect if it's a JSON string
try {
final decoded = jsonDecode(content);
const encoder = JsonEncoder.withIndent(' ');
contentStr = encoder.convert(decoded);
} catch (_) {
contentStr = content; // not a valid JSON string, keep as-is
}
} else {
contentStr = content.toString();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
const Spacer(),
IconButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: contentStr));
},
icon: const Icon(Icons.copy, size: 16),
tooltip: 'Copy',
),
],
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: Text(
contentStr,
softWrap: true,
overflow: TextOverflow.visible,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
);
}
Color _getStatusColor() {
if (log.isSuccess) {
return Colors.green;
}
if (log.isError) {
return Colors.red;
}
return Colors.orange;
}
Color _getMethodColor() {
switch (log.method.toUpperCase()) {
case 'GET':
return Colors.blue;
case 'POST':
return Colors.green;
case 'PUT':
return Colors.orange;
case 'DELETE':
return Colors.red;
default:
return Colors.grey;
}
}
String _getTimeAgo() {
final now = DateTime.now();
final diff = now.difference(log.timestamp);
if (diff.inDays > 0) {
return '${diff.inDays}d ago';
}
if (diff.inHours > 0) {
return '${diff.inHours}h ago';
}
if (diff.inMinutes > 0) {
return '${diff.inMinutes}m ago';
}
return '${diff.inSeconds}s ago';
}
}
///============================================================
/// If you use a single global Scaffold everywhere, it becomes easy to manage.
/// If you don’t, then you have to set up a separate Scaffold for every screen.
/// Example like this
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return const DraggableApiLoggerButton(
//enabled: false,
buttonColor: Colors.red,
iconColor: Colors.white,
child: Scaffold(
body: Center(
child: Column(
children: [
Text('Show in Debug button'),
],
),
),
),
);
}
}