feat: add HTML providers for Realbooru and Paheal, hidden posts screen, and UI/search enhancements

This commit is contained in:
2026-09-03 16:40:37 +03:00
parent 7cccb2cd8d
commit 45faa12133
55 changed files with 4263 additions and 2076 deletions
@@ -3,12 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../app/app.dart';
import '../../../app/responsive.dart';
import '../../../backend/backend.dart';
import '../../../shared/widgets/adaptive_scaffold.dart';
import '../../../shared/widgets/empty_view.dart';
import '../../../shared/widgets/error_view.dart';
import '../../../shared/widgets/post_masonry_grid.dart';
import '../../favorites/presentation/favorites_controller.dart';
import 'viewed_controller.dart';
@@ -21,7 +19,6 @@ class ViewedScreen extends ConsumerWidget {
final settings =
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
final favoriteKeys = ref.watch(favoriteKeysProvider).value ?? <String>{};
final viewedKeys = ref.watch(viewedKeysProvider).value ?? <String>{};
return AdaptiveScaffold(
title: settings.languageCode == 'ru' ? 'История' : 'Viewed',
actions: [
@@ -34,47 +31,111 @@ class ViewedScreen extends ConsumerWidget {
body: state.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => ErrorView(message: error.toString()),
data: (posts) => posts.isEmpty
data: (groups) => groups.isEmpty
? const EmptyView(title: 'No viewed posts yet')
: RefreshIndicator(
onRefresh: () =>
ref.read(viewedControllerProvider.notifier).refresh(),
child: PostMasonryGrid(
posts: posts,
columns: Responsive.columnsFor(
context,
mobileColumns: settings.mobileColumns,
desktopColumns: settings.desktopColumns,
),
blurExplicit: settings.blurExplicitContent,
showBadges: settings.showPostBadges,
nsfwEnabled: settings.nsfwEnabled,
mediaQualityMode:
MediaQualityMode.fromName(settings.mediaQualityMode),
favoriteKeys: favoriteKeys,
viewedKeys: viewedKeys,
onOpen: (post) => context.push(
'/post/${post.providerId}/${post.id}',
extra: post,
),
onFavorite: (post) async {
if (favoriteKeys.contains(post.cacheKey)) {
await ref
.read(favoriteServiceProvider)
.removeFavorite(post.id, post.providerId);
} else {
await ref.read(favoriteServiceProvider).addFavorite(
post,
settings: settings,
downloadManager:
ref.read(downloadManagerServiceProvider),
);
}
ref.invalidate(favoriteKeysProvider);
},
child: ListView(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 24),
children: [
for (final group in groups) ...[
Padding(
padding: const EdgeInsets.fromLTRB(4, 16, 4, 8),
child: Text(
group.label,
style: Theme.of(context).textTheme.titleMedium,
),
),
for (final item in group.items)
_ViewedPostTile(
post: item.post,
viewedAt: item.viewedAt,
isFavorite: favoriteKeys.contains(item.post.cacheKey),
onOpen: () => context.push(
'/post/${item.post.providerId}/${item.post.id}',
extra: item.post,
),
onFavorite: () => _toggleFavorite(
ref,
item.post,
settings,
favoriteKeys,
),
),
],
],
),
),
),
);
}
Future<void> _toggleFavorite(
WidgetRef ref,
Post post,
AppSettings settings,
Set<String> favoriteKeys,
) async {
if (favoriteKeys.contains(post.cacheKey)) {
await ref
.read(favoriteServiceProvider)
.removeFavorite(post.id, post.providerId);
} else {
await ref.read(favoriteServiceProvider).addFavorite(
post,
settings: settings,
downloadManager: ref.read(downloadManagerServiceProvider),
);
}
ref.invalidate(favoriteKeysProvider);
}
}
class _ViewedPostTile extends StatelessWidget {
const _ViewedPostTile({
required this.post,
required this.viewedAt,
required this.isFavorite,
required this.onOpen,
required this.onFavorite,
});
final Post post;
final DateTime viewedAt;
final bool isFavorite;
final VoidCallback onOpen;
final VoidCallback onFavorite;
@override
Widget build(BuildContext context) {
final image = MediaUrlSelector.preview(post).firstOrNull;
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
onTap: onOpen,
leading: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
width: 56,
height: 56,
child: image == null
? const Icon(Icons.image_not_supported_rounded)
: Image.network(image, fit: BoxFit.cover),
),
),
title: Text(post.tags.take(4).join(' '), maxLines: 1),
subtitle: Text(
'${post.providerName} • ${viewedAt.hour.toString().padLeft(2, '0')}:${viewedAt.minute.toString().padLeft(2, '0')}',
),
trailing: IconButton(
tooltip: isFavorite ? 'Remove favorite' : 'Favorite',
onPressed: onFavorite,
icon: Icon(isFavorite
? Icons.favorite_rounded
: Icons.favorite_border_rounded),
),
),
);
}
}