mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
- Introduced a new extension for localization in Flutter with `LocalizationExtension` in `l10n.dart`. - Added a Python script `translate.py` for translating ARB/JSON localization files using a local Ollama model, preserving keys and placeholders, and handling ICU format rules.
288 lines
8.1 KiB
Dart
288 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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|