meshcore-open/lib/widgets/gif_picker.dart
446564 b34d684e67 format dart files
formats all dart files using `dart format .` from the root project dir

this makes the code style repeatable by new contributors and makes PR review easier
2026-02-04 08:32:35 -08:00

282 lines
8.1 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../l10n/l10n.dart';
class GifPicker extends StatefulWidget {
final Function(String gifId) onGifSelected;
const GifPicker({super.key, required this.onGifSelected});
@override
State<GifPicker> createState() => _GifPickerState();
}
class _GifPickerState extends State<GifPicker> {
final TextEditingController _searchController = TextEditingController();
List<Map<String, dynamic>> _gifs = [];
bool _isLoading = false;
String? _error;
// Giphy API key - Using public beta key (limited usage)
// For production, replace with your own Giphy API key from developers.giphy.com
static const String _giphyApiKey = 'sXpGFDGZs0Dv1mmNFvYaGUvYwKX0PWIh';
@override
void initState() {
super.initState();
_loadTrendingGifs();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadTrendingGifs() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final response = await http
.get(
Uri.parse(
'https://api.giphy.com/v1/gifs/trending?api_key=$_giphyApiKey&limit=25&rating=g',
),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_gifs = List<Map<String, dynamic>>.from(data['data']);
_isLoading = false;
});
} else {
if (!mounted) return;
setState(() {
_error = context.l10n.gifPicker_failedLoad;
_isLoading = false;
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_error = context.l10n.gifPicker_noInternet;
_isLoading = false;
});
}
}
Future<void> _searchGifs(String query) async {
if (query.trim().isEmpty) {
_loadTrendingGifs();
return;
}
setState(() {
_isLoading = true;
_error = null;
});
try {
final response = await http
.get(
Uri.parse(
'https://api.giphy.com/v1/gifs/search?api_key=$_giphyApiKey&q=${Uri.encodeComponent(query)}&limit=25&rating=g',
),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_gifs = List<Map<String, dynamic>>.from(data['data']);
_isLoading = false;
});
} else {
if (!mounted) return;
setState(() {
_error = context.l10n.gifPicker_failedSearch;
_isLoading = false;
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_error = context.l10n.gifPicker_noInternet;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.7,
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Header
Row(
children: [
const Icon(Icons.gif_box, size: 28),
const SizedBox(width: 8),
Text(
context.l10n.gifPicker_title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 16),
// Search bar
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: context.l10n.gifPicker_searchHint,
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_loadTrendingGifs();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
textInputAction: TextInputAction.search,
onSubmitted: _searchGifs,
onChanged: (value) {
setState(() {}); // Update to show/hide clear button
},
),
const SizedBox(height: 16),
// GIF grid
Expanded(child: _buildContent()),
// Powered by Giphy attribution
const SizedBox(height: 8),
Text(
context.l10n.gifPicker_poweredBy,
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
);
}
Widget _buildContent() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
_error!,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _loadTrendingGifs,
icon: const Icon(Icons.refresh),
label: Text(context.l10n.common_retry),
),
],
),
);
}
if (_gifs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
context.l10n.gifPicker_noGifsFound,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
);
}
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.0,
),
itemCount: _gifs.length,
itemBuilder: (context, index) {
final gif = _gifs[index];
final gifId = gif['id'] as String;
final previewUrl =
gif['images']?['fixed_height_small']?['url'] as String?;
return GestureDetector(
onTap: () {
widget.onGifSelected(gifId);
Navigator.pop(context);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: previewUrl != null
? Image.network(
previewUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Center(child: Icon(Icons.error_outline));
},
)
: const Center(child: Icon(Icons.gif_box)),
),
),
);
},
);
}
}