You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
4.8 KiB
Dart
191 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|
|
|
class CustomizedSvg extends StatefulWidget {
|
|
/// URL of the SVG file
|
|
final String svgUrl;
|
|
|
|
/// Map of color replacements
|
|
final Map<String, String> colorReplacements;
|
|
|
|
/// Icon to show in case of error
|
|
final Widget errorIcon;
|
|
|
|
/// Width of the SVG
|
|
/// Default is 24
|
|
/// If you want to keep the aspect ratio, set only the height
|
|
final double? width;
|
|
|
|
/// Height of the SVG
|
|
/// Default is 24
|
|
/// If you want to keep the aspect ratio, set only the width
|
|
final double? height;
|
|
|
|
static final GetStorage _svgStorage = GetStorage('svg_cache');
|
|
|
|
const CustomizedSvg({
|
|
super.key,
|
|
required this.svgUrl,
|
|
required this.colorReplacements,
|
|
this.errorIcon = const Icon(Icons.error_outline),
|
|
this.width = 24,
|
|
this.height = 24,
|
|
});
|
|
|
|
@override
|
|
State<CustomizedSvg> createState() => _CustomizedSvgState();
|
|
}
|
|
|
|
class _CustomizedSvgState extends State<CustomizedSvg> {
|
|
String? _svgContent;
|
|
bool _isLoading = true;
|
|
bool _hasError = false;
|
|
bool _showProgressIndicator = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_startLoadingTimer();
|
|
_loadSvg();
|
|
}
|
|
|
|
void _startLoadingTimer() {
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
if (_isLoading && mounted) {
|
|
setState(() {
|
|
_showProgressIndicator = true;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant CustomizedSvg oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.svgUrl != widget.svgUrl) {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_hasError = false;
|
|
_showProgressIndicator = false;
|
|
});
|
|
_loadSvg();
|
|
}
|
|
}
|
|
|
|
final DateTime _cacheClearDate = DateTime(2025, 4, 13);
|
|
|
|
Future<void> _loadSvg() async {
|
|
try {
|
|
final cached = _getSvgFromCache();
|
|
if (cached != null) {
|
|
setState(() {
|
|
_svgContent = cached;
|
|
_isLoading = false;
|
|
});
|
|
return;
|
|
}
|
|
|
|
final modifiedSvg = await _fetchSvg();
|
|
setState(() {
|
|
_svgContent = modifiedSvg;
|
|
_isLoading = false;
|
|
_hasError = modifiedSvg == null;
|
|
});
|
|
} catch (_) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_hasError = true;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String?> _fetchSvg() async {
|
|
final response = await http.get(Uri.parse(widget.svgUrl));
|
|
if (response.statusCode != 200) {
|
|
final e = Exception('Failed to load SVG: ${response.statusCode}');
|
|
ErrorHandler.logError(
|
|
e: e,
|
|
data: {
|
|
"svgUrl": widget.svgUrl,
|
|
},
|
|
);
|
|
await CustomizedSvg._svgStorage.write(
|
|
widget.svgUrl,
|
|
{'timestamp': DateTime.now().millisecondsSinceEpoch},
|
|
);
|
|
throw e;
|
|
}
|
|
|
|
final String svgContent = response.body;
|
|
await CustomizedSvg._svgStorage.write(widget.svgUrl, {
|
|
'svg': svgContent,
|
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
|
});
|
|
|
|
return _modifySVG(svgContent);
|
|
}
|
|
|
|
String _modifySVG(String svgContent) {
|
|
String modifiedSvg = svgContent.replaceAll("fill=\"none\"", '');
|
|
for (final entry in widget.colorReplacements.entries) {
|
|
modifiedSvg = modifiedSvg.replaceAll(entry.key, entry.value);
|
|
}
|
|
return modifiedSvg;
|
|
}
|
|
|
|
String? _getSvgFromCache() {
|
|
final cachedSvgEntry = CustomizedSvg._svgStorage.read(widget.svgUrl);
|
|
if (cachedSvgEntry != null &&
|
|
cachedSvgEntry is Map<String, dynamic> &&
|
|
cachedSvgEntry['svg'] is String &&
|
|
cachedSvgEntry['timestamp'] is int &&
|
|
DateTime.fromMillisecondsSinceEpoch(cachedSvgEntry['timestamp'])
|
|
.isAfter(_cacheClearDate)) {
|
|
return _modifySVG(cachedSvgEntry['svg'] as String);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoading) {
|
|
if (_showProgressIndicator) {
|
|
return SizedBox(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
child: const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
} else {
|
|
return SizedBox(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
);
|
|
}
|
|
} else if (_hasError || _svgContent == null) {
|
|
return widget.errorIcon;
|
|
} else {
|
|
return SvgPicture.string(
|
|
_svgContent!,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
String colorToHex(Color color) {
|
|
return '#'
|
|
'${(color.r * 255).toInt().toRadixString(16).padLeft(2, '0')}'
|
|
'${(color.g * 255).toInt().toRadixString(16).padLeft(2, '0')}'
|
|
'${(color.b * 255).toInt().toRadixString(16).padLeft(2, '0')}';
|
|
}
|