feat: add HTML providers for Realbooru and Paheal, hidden posts screen, and UI/search enhancements
This commit is contained in:
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user