gif_service.dart
1 import 'dart:convert'; 2 import 'dart:io' show Platform; 3 import 'dart:typed_data'; 4 5 import 'package:http/http.dart' as http; 6 7 const _iosKey = String.fromEnvironment('GIPHY_API_KEY_IOS'); 8 const _androidKey = String.fromEnvironment('GIPHY_API_KEY_ANDROID'); 9 const _genericKey = String.fromEnvironment('GIPHY_API_KEY'); 10 final String _apiKey = Platform.isIOS ? (_iosKey.isNotEmpty ? _iosKey : _genericKey) 11 : Platform.isAndroid ? (_androidKey.isNotEmpty ? _androidKey : _genericKey) 12 : _genericKey; 13 const _baseUrl = 'https://api.giphy.com/v1/gifs'; 14 15 class GifItem { 16 final String id; 17 final String previewUrl; 18 final int previewWidth; 19 final int previewHeight; 20 final String fullUrl; 21 22 const GifItem({ 23 required this.id, 24 required this.previewUrl, 25 required this.previewWidth, 26 required this.previewHeight, 27 required this.fullUrl, 28 }); 29 } 30 31 class GifService { 32 final http.Client _client = http.Client(); 33 34 bool get hasApiKey => _apiKey.isNotEmpty; 35 36 Future<List<GifItem>> search(String query, {int limit = 20}) async { 37 final uri = Uri.parse('$_baseUrl/search').replace(queryParameters: { 38 'api_key': _apiKey, 39 'q': query, 40 'limit': '$limit', 41 'rating': 'pg-13', 42 }); 43 return _fetchGifs(uri); 44 } 45 46 Future<List<GifItem>> trending({int limit = 20}) async { 47 final uri = Uri.parse('$_baseUrl/trending').replace(queryParameters: { 48 'api_key': _apiKey, 49 'limit': '$limit', 50 'rating': 'pg-13', 51 }); 52 return _fetchGifs(uri); 53 } 54 55 Future<List<GifItem>> _fetchGifs(Uri uri) async { 56 if (!hasApiKey) { 57 throw Exception('GIPHY_API_KEY not configured. Build with --dart-define=GIPHY_API_KEY=your_key'); 58 } 59 60 final response = await _client.get(uri); 61 if (response.statusCode != 200) { 62 final body = response.body; 63 throw Exception('Giphy API error ${response.statusCode}: $body'); 64 } 65 66 final data = jsonDecode(response.body) as Map<String, dynamic>; 67 final results = data['data'] as List<dynamic>? ?? []; 68 69 return results.map((item) { 70 final images = item['images'] as Map<String, dynamic>; 71 // fixed_width_small (~100px) for grid preview 72 final preview = images['fixed_width_small'] as Map<String, dynamic>; 73 // downsized (under 2MB) for sending — good quality, fast to decode 74 final full = (images['downsized'] ?? images['downsized_medium']) as Map<String, dynamic>; 75 return GifItem( 76 id: item['id'] as String, 77 previewUrl: preview['url'] as String, 78 previewWidth: int.parse(preview['width'] as String), 79 previewHeight: int.parse(preview['height'] as String), 80 fullUrl: full['url'] as String, 81 ); 82 }).toList(); 83 } 84 85 Future<Uint8List> downloadGif(String url) async { 86 final response = await _client.get(Uri.parse(url)); 87 if (response.statusCode != 200) { 88 throw Exception('Failed to download GIF: ${response.statusCode}'); 89 } 90 return response.bodyBytes; 91 } 92 93 void dispose() { 94 _client.close(); 95 } 96 }