Polish mobile UX downloads and comments

This commit is contained in:
2026-05-23 12:15:20 +03:00
parent 63da741295
commit f9938e393d
24 changed files with 571 additions and 61 deletions
@@ -1,4 +1,5 @@
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
@@ -19,3 +20,7 @@ abstract class ContentProvider {
Future<ProviderHealth> checkHealth();
}
abstract class CommentProvider {
Future<List<PostComment>> getComments(String postId);
}
+11 -1
View File
@@ -1,10 +1,11 @@
import '../../core/errors/app_exception.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import 'content_provider.dart';
class CustomProvider implements ContentProvider {
class CustomProvider implements ContentProvider, CommentProvider {
CustomProvider(this._delegate);
final ContentProvider _delegate;
@@ -37,6 +38,15 @@ class CustomProvider implements ContentProvider {
@override
Future<ProviderHealth> checkHealth() => _delegate.checkHealth();
@override
Future<List<PostComment>> getComments(String postId) {
final delegate = _delegate;
if (delegate is CommentProvider) {
return (delegate as CommentProvider).getComments(postId);
}
return Future.value(const []);
}
}
class UnsupportedCustomProvider implements ContentProvider {
+34 -1
View File
@@ -3,11 +3,12 @@ import 'package:dio/dio.dart';
import '../../core/http/dio_client.dart';
import '../mappers/danbooru_mapper.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import 'content_provider.dart';
class DanbooruProvider implements ContentProvider {
class DanbooruProvider implements ContentProvider, CommentProvider {
DanbooruProvider({
required this.id,
required this.name,
@@ -96,6 +97,38 @@ class DanbooruProvider implements ContentProvider {
}
}
@override
Future<List<PostComment>> getComments(String postId) async {
final response = await _dio.get<dynamic>(
'/comments.json',
queryParameters: {
'search[post_id]': postId,
'limit': 50,
..._queryParameters,
},
);
final data = response.data;
final items = data is List ? data : const [];
return items
.whereType<Map>()
.map((item) {
final json = Map<String, dynamic>.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();
}
List<String> _topTags(TopPeriodFilter period) {
return switch (period) {
TopPeriodFilter.none => const [],
+50 -1
View File
@@ -4,11 +4,12 @@ import '../../core/errors/app_exception.dart';
import '../../core/http/dio_client.dart';
import '../mappers/gelbooru_mapper.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import 'content_provider.dart';
class GelbooruProvider implements ContentProvider {
class GelbooruProvider implements ContentProvider, CommentProvider {
GelbooruProvider({
required this.id,
required this.name,
@@ -117,6 +118,22 @@ class GelbooruProvider implements ContentProvider {
}
}
@override
Future<List<PostComment>> getComments(String postId) async {
final response = await _dio.get<dynamic>(
'/index.php',
queryParameters: {
'page': 'dapi',
's': 'comment',
'q': 'index',
'json': '1',
'post_id': postId,
..._queryParameters,
},
);
return _commentsFromResponse(response.data, postId);
}
Never unavailable(String message) =>
throw ProviderUnavailableException(message);
@@ -129,4 +146,36 @@ class GelbooruProvider implements ContentProvider {
const ['sort:score:desc'],
};
}
List<PostComment> _commentsFromResponse(dynamic data, String postId) {
final raw = switch (data) {
List() => data,
{'comments': final List comments} => comments,
{'@attributes': _, 'comment': final List comments} => comments,
{'comment': final List comments} => comments,
_ => const [],
};
return raw
.whereType<Map>()
.map((item) {
final json = Map<String, dynamic>.from(item);
return PostComment(
id: (json['id'] ?? json['comment_id'] ?? '').toString(),
postId: (json['post_id'] ?? postId).toString(),
providerId: id,
authorName: (json['creator'] ??
json['creator_name'] ??
json['author'] ??
'')
.toString(),
body: (json['body'] ?? json['comment'] ?? '').toString(),
createdAt: DateTime.tryParse(
(json['created_at'] ?? json['created'] ?? '').toString(),
) ??
DateTime.fromMillisecondsSinceEpoch(0),
);
})
.where((comment) => comment.body.trim().isNotEmpty)
.toList();
}
}
+14 -10
View File
@@ -20,34 +20,38 @@ class ProviderFactory {
timeout: Duration(seconds: config.timeoutSeconds),
headers: headers,
);
return switch (config.apiType.toLowerCase()) {
'gelbooru' => GelbooruProvider(
switch (config.apiType.toLowerCase()) {
case 'gelbooru':
return GelbooruProvider(
id: config.id,
name: config.name,
baseUrl: config.baseUrl,
dioClient: client,
queryParameters: queryParameters,
),
'rule34' => Rule34Provider(
);
case 'rule34':
return Rule34Provider(
id: config.id,
name: config.name,
baseUrl: config.baseUrl,
dioClient: client,
queryParameters: queryParameters,
),
'danbooru' => DanbooruProvider(
);
case 'danbooru':
return DanbooruProvider(
id: config.id,
name: config.name,
baseUrl: config.baseUrl,
dioClient: client,
queryParameters: queryParameters,
),
_ => UnsupportedCustomProvider(
);
default:
return UnsupportedCustomProvider(
id: config.id,
name: config.name,
baseUrl: config.baseUrl,
apiType: config.apiType,
),
};
);
}
}
}
+31 -5
View File
@@ -4,6 +4,7 @@ import '../../core/errors/failure.dart';
import '../../core/utils/result.dart';
import '../models/content_provider_config.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import '../repositories/provider_repository.dart';
@@ -96,11 +97,6 @@ class ProviderManager {
final posts = <Post>[];
for (final provider in providers) {
final health = await _repository.getHealth(provider.id);
final offline = health is Success<ProviderHealth?> &&
health.data?.status == ProviderStatus.offline;
if (offline) continue;
try {
final providerPosts = await provider.searchPosts(
tags: tags,
@@ -125,6 +121,36 @@ class ProviderManager {
return Success(posts);
}
Future<Result<List<PostComment>>> getComments(
String providerId,
String postId,
) async {
final providersResult = await activeProviders();
if (providersResult is Error<List<ContentProvider>>) {
return Error(providersResult.failure);
}
final providers = (providersResult as Success<List<ContentProvider>>).data;
final matches = providers.where((provider) => provider.id == providerId);
if (matches.isEmpty) {
return const Error(
Failure(code: 'not_found', message: 'Provider not found'),
);
}
final provider = matches.first;
if (provider is! CommentProvider) return const Success([]);
try {
return Success(await (provider as CommentProvider).getComments(postId));
} catch (error) {
return Error(
Failure(
code: 'comments_unavailable',
message: 'Comments unavailable',
details: error,
),
);
}
}
Future<Result<Post?>> getPost(String providerId, String postId) async {
final providersResult = await activeProviders();
if (providersResult is Error<List<ContentProvider>>) {