| import 'dart:convert'; |
| import 'package:jaspr/dom.dart'; |
| import 'package:jaspr/jaspr.dart'; |
| import '../../../utils/styles.dart'; |
| import '../../workspace/data/workspace_repository.dart'; |
| |
| class ImagePreview extends StatefulComponent { |
| const ImagePreview({required this.path, required this.workspaceRepository, super.key}); |
| |
| final String path; |
| final WorkspaceRepository workspaceRepository; |
| |
| @override |
| State<ImagePreview> createState() => _ImagePreviewState(); |
| |
| @css |
| static List<StyleRule> get styles => _ImagePreviewState.styles; |
| } |
| |
| class _ImagePreviewState extends State<ImagePreview> { |
| String? _dataUrl; |
| String? _error; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _loadImage(); |
| } |
| |
| @override |
| void didUpdateComponent(ImagePreview oldComponent) { |
| super.didUpdateComponent(oldComponent); |
| if (oldComponent.path != component.path) { |
| _loadImage(); |
| } |
| } |
| |
| Future<void> _loadImage() async { |
| setState(() { |
| _dataUrl = null; |
| _error = null; |
| }); |
| |
| try { |
| final bytes = await component.workspaceRepository.workspaceResourceApi.readFileAsBytes(component.path); |
| final ext = component.path.split('.').last.toLowerCase(); |
| final mime = switch (ext) { |
| 'png' => 'image/png', |
| 'jpg' || 'jpeg' => 'image/jpeg', |
| 'gif' => 'image/gif', |
| 'webp' => 'image/webp', |
| 'svg' => 'image/svg+xml', |
| 'bmp' => 'image/bmp', |
| 'ico' => 'image/x-icon', |
| _ => 'application/octet-stream', |
| }; |
| |
| final base64String = base64Encode(bytes); |
| setState(() { |
| _dataUrl = 'data:$mime;base64,$base64String'; |
| }); |
| } catch (e) { |
| setState(() { |
| _error = e.toString(); |
| }); |
| } |
| } |
| |
| @override |
| Component build(BuildContext context) { |
| if (_error != null) { |
| return div(classes: 'image-preview-error', [ |
| const Component.text('Failed to load image: '), |
| Component.text(_error!), |
| ]); |
| } |
| |
| if (_dataUrl == null) { |
| return const div(classes: 'image-preview-loading', [ |
| Component.text('Loading image...'), |
| ]); |
| } |
| |
| return div(classes: 'image-preview-container', [ |
| img(src: _dataUrl!, classes: 'image-preview-el'), |
| ]); |
| } |
| |
| static List<StyleRule> get styles => [ |
| css('.image-preview-container').styles( |
| display: .flex, |
| width: 100.percent, |
| height: 100.percent, |
| padding: .all(20.px), |
| boxSizing: .borderBox, |
| overflow: const Overflow.only(x: OverflowValue.auto, y: OverflowValue.auto), |
| justifyContent: .center, |
| alignItems: .center, |
| backgroundColor: colorContainer, |
| ), |
| css('.image-preview-el').styles( |
| maxWidth: 100.percent, |
| maxHeight: 100.percent, |
| radius: .all(Radius.circular(4.px)), |
| shadow: BoxShadow(offsetX: 0.px, offsetY: 4.px, blur: 12.px, color: const Color.rgba(0, 0, 0, 0.15)), |
| raw: {'object-fit': 'contain'}, |
| ), |
| css('.image-preview-loading').styles( |
| display: .flex, |
| width: 100.percent, |
| height: 100.percent, |
| justifyContent: .center, |
| alignItems: .center, |
| color: colorOnSurfaceVariant, |
| fontSize: 14.px, |
| ), |
| css('.image-preview-error').styles( |
| display: .flex, |
| width: 100.percent, |
| height: 100.percent, |
| justifyContent: .center, |
| alignItems: .center, |
| color: colorError, |
| fontSize: 14.px, |
| ), |
| ]; |
| } |