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
@@ -0,0 +1,130 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../app/app.dart';
import '../../../backend/backend.dart';
import '../../../core/utils/result.dart';
import '../../../shared/widgets/adaptive_scaffold.dart';
import 'settings_controller.dart';
final hiddenPostRowsProvider = FutureProvider<List<HiddenPostRow>>((ref) async {
final settings = ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
final repository = ref.watch(postRepositoryProvider);
final rows = <HiddenPostRow>[];
for (final key in settings.hiddenPostKeys) {
final parts = key.split(':');
Post? post;
if (parts.length >= 2) {
final result = await repository.getCachedPost(
parts.sublist(1).join(':'), parts.first);
if (result is Success<Post?>) post = result.data;
}
rows.add(HiddenPostRow(cacheKey: key, post: post));
}
return rows;
});
class HiddenPostsScreen extends ConsumerWidget {
const HiddenPostsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final rows = ref.watch(hiddenPostRowsProvider);
return AdaptiveScaffold(
title: 'Hidden posts',
actions: [
IconButton(
tooltip: 'Restore all',
onPressed: rows.valueOrNull?.isEmpty ?? true
? null
: () async {
await ref.read(settingsServiceProvider).clearHiddenPosts();
ref.invalidate(appSettingsProvider);
ref.invalidate(settingsControllerProvider);
ref.invalidate(hiddenPostRowsProvider);
},
icon: const Icon(Icons.restore_rounded),
),
],
body: rows.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text(error.toString())),
data: (items) => items.isEmpty
? const Center(child: Text('No hidden posts'))
: ListView.builder(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 24),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return _HiddenPostTile(
item: item,
onOpen: item.post == null
? null
: () => context.push(
'/post/${item.post!.providerId}/${item.post!.id}',
extra: item.post,
),
onRestore: () async {
await ref
.read(settingsServiceProvider)
.unhidePostKey(item.cacheKey);
ref.invalidate(appSettingsProvider);
ref.invalidate(settingsControllerProvider);
ref.invalidate(hiddenPostRowsProvider);
},
);
},
),
),
);
}
}
class HiddenPostRow {
const HiddenPostRow({required this.cacheKey, this.post});
final String cacheKey;
final Post? post;
}
class _HiddenPostTile extends StatelessWidget {
const _HiddenPostTile({
required this.item,
required this.onRestore,
this.onOpen,
});
final HiddenPostRow item;
final VoidCallback onRestore;
final VoidCallback? onOpen;
@override
Widget build(BuildContext context) {
final post = item.post;
final imageUrl =
post == null ? null : MediaUrlSelector.preview(post).firstOrNull;
return Card(
child: ListTile(
onTap: onOpen,
leading: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
width: 56,
height: 56,
child: imageUrl == null
? const Icon(Icons.visibility_off_rounded)
: CachedNetworkImage(imageUrl: imageUrl, fit: BoxFit.cover),
),
),
title: Text(post?.tags.take(4).join(' ') ?? item.cacheKey),
subtitle: Text(post?.providerName ?? item.cacheKey),
trailing: TextButton(
onPressed: onRestore,
child: const Text('Restore'),
),
),
);
}
}
@@ -101,8 +101,12 @@ class _SettingsContent extends ConsumerWidget {
),
SwitchListTile(
value: settings.autoDownloadFavorites,
title: Text(strings.autoDownloadFavorites),
subtitle: Text(strings.autoDownloadFavoritesHint),
title: Text(settings.languageCode == 'ru'
? 'Offline избранное'
: 'Offline favorites'),
subtitle: Text(settings.languageCode == 'ru'
? 'При добавлении фото или видео в избранное файл скачивается в фоне для просмотра офлайн.'
: 'When a photo or video is added to Favorites, Lunaris downloads it in the background for offline viewing.'),
onChanged: (value) => _update(
ref,
settings.copyWith(autoDownloadFavorites: value),
@@ -283,6 +287,13 @@ class _SettingsContent extends ConsumerWidget {
icon: const Icon(Icons.history_toggle_off_rounded),
label: Text(strings.clearViewed),
),
FilledButton.tonalIcon(
onPressed: settings.hiddenPostKeys.isEmpty
? null
: () => context.go('/settings/hidden'),
icon: const Icon(Icons.restore_rounded),
label: const Text('Restore hidden'),
),
FilledButton.tonalIcon(
onPressed: () => ref
.read(settingsControllerProvider.notifier)
@@ -741,13 +752,19 @@ class _ColorSwatches extends StatelessWidget {
final int selected;
final ValueChanged<int> onChanged;
static const colors = <int>[
0xFFE84D8A,
0xFF8B5CF6,
0xFF3B82F6,
0xFF14B8A6,
0xFFF97316,
0xFFEF4444,
static const colors = <(int, String)>[
(0xFFE84D8A, 'Lunaris pink'),
(0xFF8B5CF6, 'Violet'),
(0xFF3B82F6, 'Azure'),
(0xFF0EA5E9, 'Sky'),
(0xFF14B8A6, 'Teal'),
(0xFF22C55E, 'Green'),
(0xFFEAB308, 'Amber'),
(0xFFF97316, 'Orange'),
(0xFFEF4444, 'Red'),
(0xFFEC4899, 'Hot pink'),
(0xFF6366F1, 'Indigo'),
(0xFF64748B, 'Slate'),
];
@override
@@ -761,28 +778,28 @@ class _ColorSwatches extends StatelessWidget {
spacing: 10,
runSpacing: 10,
children: [
for (final value in colors)
for (final entry in colors)
Tooltip(
message:
'#${value.toRadixString(16).substring(2).toUpperCase()}',
'${entry.$2} #${entry.$1.toRadixString(16).substring(2).toUpperCase()}',
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: () => onChanged(value),
onTap: () => onChanged(entry.$1),
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(value),
color: Color(entry.$1),
shape: BoxShape.circle,
border: Border.all(
color: selected == value
color: selected == entry.$1
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.outlineVariant,
width: selected == value ? 3 : 1,
width: selected == entry.$1 ? 3 : 1,
),
),
child: SizedBox(
width: 38,
height: 38,
child: selected == value
child: selected == entry.$1
? const Icon(Icons.check_rounded, color: Colors.white)
: null,
),