import 'package:dio/dio.dart'; import '../../core/http/dio_client.dart'; import '../mappers/e621_mapper.dart'; import '../models/post.dart'; import '../models/post_comment.dart'; import '../models/provider_health.dart'; import '../models/tag_suggestion.dart'; import '../models/top_period_filter.dart'; import 'content_provider.dart'; class E621Provider implements ContentProvider, TagSuggestionProvider, CommentProvider, PostPageProvider { E621Provider({ required this.id, required this.name, required this.baseUrl, required DioClient dioClient, Map queryParameters = const {}, }) : _dio = dioClient.dio, _queryParameters = queryParameters; @override final String id; @override final String name; @override final String baseUrl; final Dio _dio; final Map _queryParameters; @override String postPageUrl(Post post) => '$baseUrl/posts/${post.id}'; @override Future> searchPosts({ required List tags, required int page, int limit = 50, String? rating, TopPeriodFilter topPeriod = TopPeriodFilter.none, }) async { final response = await _dio.get( '/posts.json', queryParameters: { 'page': page + 1, 'limit': limit.clamp(1, 75), 'tags': [ ...tags, if (rating != null && rating.isNotEmpty) 'rating:${_rating(rating)}', ..._topTags(topPeriod), ].join(' '), ..._queryParameters, }, ); return E621Mapper.postsFromResponse( response.data, providerId: id, providerName: name, ); } @override Future getPost(String id) async { final response = await _dio.get( '/posts/$id.json', queryParameters: _queryParameters, ); final posts = E621Mapper.postsFromResponse( response.data, providerId: this.id, providerName: name, ); return posts.isEmpty ? null : posts.first; } @override Future checkHealth() async { final startedAt = DateTime.now(); try { await _dio.get( '/posts.json', queryParameters: {'limit': 1, ..._queryParameters}, ); return ProviderHealth( providerId: id, status: ProviderStatus.online, pingMs: DateTime.now().difference(startedAt).inMilliseconds, lastCheckedAt: DateTime.now(), apiVersion: 'e621', ); } catch (error) { return ProviderHealth( providerId: id, status: ProviderStatus.offline, pingMs: DateTime.now().difference(startedAt).inMilliseconds, lastCheckedAt: DateTime.now(), errorMessage: error.toString(), apiVersion: 'e621', ); } } @override Future> suggestTags(String query, {int limit = 20}) async { if (query.trim().isEmpty) return const []; final response = await _dio.get( '/tags.json', queryParameters: { 'search[name_matches]': '${query.trim()}*', 'search[order]': 'count', 'limit': limit.clamp(1, 50), ..._queryParameters, }, ); final items = response.data is List ? response.data as List : const []; return items .whereType() .map((item) { final json = Map.from(item); return TagSuggestion( name: (json['name'] ?? '').toString(), category: tagCategoryFromString(json['category']?.toString()), postCount: _int(json['post_count']), providerId: id, ); }) .where((tag) => tag.name.isNotEmpty) .toList(); } @override Future> getComments(String postId) async { final response = await _dio.get( '/comments.json', queryParameters: { 'search[post_id]': postId, 'limit': 50, ..._queryParameters, }, ); final items = response.data is List ? response.data as List : const []; return items .whereType() .map((item) { final json = Map.from(item); return PostComment( id: (json['id'] ?? '').toString(), postId: (json['post_id'] ?? postId).toString(), providerId: id, authorName: (json['creator_name'] ?? json['creator_id'] ?? 'user') .toString(), body: (json['body'] ?? '').toString(), createdAt: DateTime.tryParse((json['created_at'] ?? '').toString()) ?? DateTime.fromMillisecondsSinceEpoch(0), ); }) .where((comment) => comment.body.trim().isNotEmpty) .toList(); } String _rating(String value) { return switch (value.toLowerCase()) { 'safe' => 's', 'questionable' => 'q', 'explicit' => 'e', _ => value, }; } List _topTags(TopPeriodFilter period) { final now = DateTime.now(); return switch (period) { TopPeriodFilter.none => const [], TopPeriodFilter.month => [ 'order:score', 'date:>${_date(now.subtract(const Duration(days: 31)))}', ], TopPeriodFilter.year => [ 'order:score', 'date:>${_date(DateTime(now.year - 1, now.month, now.day))}', ], TopPeriodFilter.allTime => const ['order:score'], }; } String _date(DateTime value) { return '${value.year.toString().padLeft(4, '0')}-' '${value.month.toString().padLeft(2, '0')}-' '${value.day.toString().padLeft(2, '0')}'; } int _int(dynamic value) { if (value is int) return value; if (value is num) return value.toInt(); return int.tryParse(value?.toString() ?? '') ?? 0; } }