mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
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
282 lines
8.1 KiB
Dart
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)),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|