diff --git a/installer/RuleGel.iss b/installer/RuleGel.iss index f0ab8c5..40ab34c 100644 --- a/installer/RuleGel.iss +++ b/installer/RuleGel.iss @@ -1,5 +1,5 @@ #define MyAppName "RuleGel" -#define MyAppVersion "0.4.3" +#define MyAppVersion "1.0.0" #define MyAppPublisher "RarDog" #define MyAppExeName "gel_rule_app.exe" diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart index 18e7bd3..43e087a 100644 --- a/lib/app/app_version.dart +++ b/lib/app/app_version.dart @@ -1,2 +1,2 @@ -const appDisplayVersion = '0.4.3'; -const appBuildNumber = 7; +const appDisplayVersion = '1.0.0'; +const appBuildNumber = 8; diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart new file mode 100644 index 0000000..5c6d661 --- /dev/null +++ b/lib/app/changelog.dart @@ -0,0 +1,34 @@ +const ruleGelChangelog = [ + RuleGelChange( + version: '1.0.0', + title: 'Stable 1.0', + bullets: [ + 'Local hide-post organization for posts you do not want in the feed.', + 'Diagnostics report and local log copy tools in Settings.', + 'Cleaner release metadata and maintenance tools for troubleshooting.', + 'Windows portable package is published alongside APK and installer.', + ], + ), + RuleGelChange( + version: '0.4.3', + title: 'Bugfixes', + bullets: [ + 'Fixed video play/retry controls.', + 'Fixed tag chip input and tag navigation from details.', + 'Fixed collection deletion persistence.', + 'Settings export now includes blacklist and whitelist data.', + ], + ), +]; + +class RuleGelChange { + const RuleGelChange({ + required this.version, + required this.title, + required this.bullets, + }); + + final String version; + final String title; + final List bullets; +} diff --git a/lib/backend/di/backend_providers.dart b/lib/backend/di/backend_providers.dart index 2347568..72b71a1 100644 --- a/lib/backend/di/backend_providers.dart +++ b/lib/backend/di/backend_providers.dart @@ -168,7 +168,7 @@ final updateServiceProvider = Provider((ref) { connectTimeout: const Duration(seconds: 12), receiveTimeout: const Duration(seconds: 12), headers: const { - 'User-Agent': 'RuleGel/0.4 Flutter local booru browser', + 'User-Agent': 'RuleGel/1.0 Flutter local booru browser', 'Accept': 'application/json', }, ), diff --git a/lib/backend/services/feed_service.dart b/lib/backend/services/feed_service.dart index 4d71431..13afc02 100644 --- a/lib/backend/services/feed_service.dart +++ b/lib/backend/services/feed_service.dart @@ -122,6 +122,8 @@ bool postMatchesRequestedTags(Post post, List requestedTags) { } bool postPassesTagFilters(Post post, AppSettings settings) { + if (settings.hiddenPostKeys.contains(post.cacheKey)) return false; + final tags = _postTagSet(post); if (tags.isEmpty) return settings.whitelistedTags.isEmpty; diff --git a/lib/backend/services/settings_service.dart b/lib/backend/services/settings_service.dart index fbcc222..0cd4a1a 100644 --- a/lib/backend/services/settings_service.dart +++ b/lib/backend/services/settings_service.dart @@ -7,6 +7,7 @@ import '../../core/database/database_service.dart'; import '../../core/errors/failure.dart'; import '../../core/utils/result.dart'; import '../models/content_provider_config.dart'; +import '../models/provider_diagnostics.dart'; import '../repositories/provider_repository.dart'; class AppSettings { @@ -29,6 +30,8 @@ class AppSettings { required this.smartBlacklistRules, required this.hideViewedPosts, required this.mediaQualityMode, + required this.hiddenPostKeys, + required this.diagnosticLogLines, required this.lastFeedTags, required this.lastFeedProviderIds, required this.lastFeedTopPeriod, @@ -57,6 +60,8 @@ class AppSettings { final List smartBlacklistRules; final bool hideViewedPosts; final String mediaQualityMode; + final List hiddenPostKeys; + final List diagnosticLogLines; final List lastFeedTags; final List lastFeedProviderIds; final String lastFeedTopPeriod; @@ -85,6 +90,8 @@ class AppSettings { smartBlacklistRules: [], hideViewedPosts: false, mediaQualityMode: 'auto', + hiddenPostKeys: [], + diagnosticLogLines: [], lastFeedTags: [], lastFeedProviderIds: [], lastFeedTopPeriod: 'none', @@ -114,6 +121,8 @@ class AppSettings { List? smartBlacklistRules, bool? hideViewedPosts, String? mediaQualityMode, + List? hiddenPostKeys, + List? diagnosticLogLines, List? lastFeedTags, List? lastFeedProviderIds, String? lastFeedTopPeriod, @@ -145,6 +154,8 @@ class AppSettings { smartBlacklistRules: smartBlacklistRules ?? this.smartBlacklistRules, hideViewedPosts: hideViewedPosts ?? this.hideViewedPosts, mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode, + hiddenPostKeys: hiddenPostKeys ?? this.hiddenPostKeys, + diagnosticLogLines: diagnosticLogLines ?? this.diagnosticLogLines, lastFeedTags: lastFeedTags ?? this.lastFeedTags, lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds, lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod, @@ -176,6 +187,8 @@ class AppSettings { 'smartBlacklistRules': smartBlacklistRules, 'hideViewedPosts': hideViewedPosts, 'mediaQualityMode': mediaQualityMode, + 'hiddenPostKeys': hiddenPostKeys, + 'diagnosticLogLines': diagnosticLogLines, 'lastFeedTags': lastFeedTags, 'lastFeedProviderIds': lastFeedProviderIds, 'lastFeedTopPeriod': lastFeedTopPeriod, @@ -230,6 +243,12 @@ class AppSettings { (json['hideViewedPosts'] as bool?) ?? defaults.hideViewedPosts, mediaQualityMode: (json['mediaQualityMode'] as String?) ?? defaults.mediaQualityMode, + hiddenPostKeys: List.from( + (json['hiddenPostKeys'] as List?) ?? defaults.hiddenPostKeys, + ), + diagnosticLogLines: List.from( + (json['diagnosticLogLines'] as List?) ?? defaults.diagnosticLogLines, + ), lastFeedTags: List.from( (json['lastFeedTags'] as List?) ?? defaults.lastFeedTags, ), @@ -331,6 +350,124 @@ class SettingsService { ); } + Future> hidePostKey(String cacheKey) async { + final result = await getSettings(); + if (result is Error) return Error(result.failure); + final settings = (result as Success).data; + final hidden = {...settings.hiddenPostKeys, cacheKey}.toList() + ..sort(); + return updateSettings(settings.copyWith(hiddenPostKeys: hidden)); + } + + Future> unhidePostKey(String cacheKey) async { + final result = await getSettings(); + if (result is Error) return Error(result.failure); + final settings = (result as Success).data; + return updateSettings( + settings.copyWith( + hiddenPostKeys: + settings.hiddenPostKeys.where((key) => key != cacheKey).toList(), + ), + ); + } + + Future> clearHiddenPosts() async { + final result = await getSettings(); + if (result is Error) return Error(result.failure); + final settings = (result as Success).data; + return updateSettings(settings.copyWith(hiddenPostKeys: const [])); + } + + Future> appendDiagnosticLog(String message) async { + final result = await getSettings(); + if (result is Error) return Error(result.failure); + final settings = (result as Success).data; + final line = '${DateTime.now().toIso8601String()} $message'; + final lines = [...settings.diagnosticLogLines, line]; + return updateSettings( + settings.copyWith( + diagnosticLogLines: + lines.length > 200 ? lines.sublist(lines.length - 200) : lines, + ), + ); + } + + Future> clearDiagnosticLogs() async { + final result = await getSettings(); + if (result is Error) return Error(result.failure); + final settings = (result as Success).data; + return updateSettings(settings.copyWith(diagnosticLogLines: const [])); + } + + Future> buildDiagnosticsReport({ + required String appVersion, + required int buildNumber, + }) async { + final settingsResult = await getSettings(); + if (settingsResult is Error) { + return Error(settingsResult.failure); + } + final settings = (settingsResult as Success).data; + final providers = []; + final diagnostics = []; + final repository = _providerRepository; + if (repository != null) { + final providersResult = await repository.getProviders(enabledOnly: false); + if (providersResult is Success>) { + providers.addAll(providersResult.data); + } + final diagnosticsResult = await repository.getDiagnostics(); + if (diagnosticsResult is Success>) { + for (final item in diagnosticsResult.data) { + diagnostics.add( + '${item.providerId}: ${item.lastResultCount} posts, ' + 'lastSearchAt=${item.lastSearchAt}, ' + 'error=${item.lastErrorMessage ?? 'none'}', + ); + } + } + } + final enabledProviders = + providers.where((provider) => provider.enabled).map((p) => p.id); + return Success( + const JsonEncoder.withIndent(' ').convert({ + 'app': { + 'name': 'RuleGel', + 'version': appVersion, + 'build': buildNumber, + 'generatedAt': DateTime.now().toIso8601String(), + }, + 'settingsSummary': { + 'themeMode': settings.themeMode, + 'nsfwEnabled': settings.nsfwEnabled, + 'blurExplicitContent': settings.blurExplicitContent, + 'mediaQualityMode': settings.mediaQualityMode, + 'cacheTtlHours': settings.cacheTtlHours, + 'cacheMaxItems': settings.cacheMaxItems, + 'hiddenPosts': settings.hiddenPostKeys.length, + 'blacklistRules': settings.smartBlacklistRules.length, + 'whitelistTags': settings.whitelistedTags.length, + 'hideViewedPosts': settings.hideViewedPosts, + }, + 'providers': { + 'enabled': enabledProviders.toList(), + 'all': providers + .map((provider) => { + 'id': provider.id, + 'name': provider.name, + 'apiType': provider.apiType, + 'enabled': provider.enabled, + 'priority': provider.priority, + 'baseUrl': provider.baseUrl, + }) + .toList(), + }, + 'recentLogs': settings.diagnosticLogLines.take(60).toList(), + 'providerDiagnostics': diagnostics, + }), + ); + } + Future> exportSettingsToJson() async { final result = await getSettings(); if (result is Error) return Error(result.failure); @@ -351,6 +488,7 @@ class SettingsService { 'blacklistedTags': settings.blacklistedTags, 'whitelistedTags': settings.whitelistedTags, 'smartBlacklistRules': settings.smartBlacklistRules, + 'hiddenPostKeys': settings.hiddenPostKeys, }, 'providers': providers.map((provider) => provider.toJson()).toList(), }), diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index 8501bd9..d03c0ca 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -3,7 +3,17 @@ import 'package:flutter/foundation.dart'; class AppLogger { const AppLogger(); + static final List _lines = []; + + static List get lines => List.unmodifiable(_lines); + + static void clear() => _lines.clear(); + void debug(String message, [Object? error, StackTrace? stackTrace]) { + final line = '[${DateTime.now().toIso8601String()}] $message' + '${error == null ? '' : ' error=$error'}'; + _lines.add(line); + if (_lines.length > 200) _lines.removeRange(0, _lines.length - 200); if (kDebugMode) { debugPrint('[GelRule] $message'); if (error != null) debugPrint(' error: $error'); diff --git a/lib/features/feed/presentation/feed_controller.dart b/lib/features/feed/presentation/feed_controller.dart index a39f655..75839c0 100644 --- a/lib/features/feed/presentation/feed_controller.dart +++ b/lib/features/feed/presentation/feed_controller.dart @@ -1,5 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../app/app.dart'; import '../../../backend/backend.dart'; import '../../../core/utils/result.dart'; import 'feed_state.dart'; @@ -153,6 +154,22 @@ class FeedController extends AsyncNotifier { await refresh(); } + Future hidePost(Post post) async { + final current = state.value; + if (current == null) return; + final result = + await ref.read(settingsServiceProvider).hidePostKey(post.cacheKey); + if (result is Error) return; + state = AsyncData( + current.copyWith( + posts: current.posts + .where((item) => item.cacheKey != post.cacheKey) + .toList(growable: false), + ), + ); + ref.invalidate(appSettingsProvider); + } + Future saveSession({double? scrollOffset}) async { final current = state.value; if (current == null) return; diff --git a/lib/features/feed/presentation/feed_screen.dart b/lib/features/feed/presentation/feed_screen.dart index f144d5b..516b2fe 100644 --- a/lib/features/feed/presentation/feed_screen.dart +++ b/lib/features/feed/presentation/feed_screen.dart @@ -249,6 +249,7 @@ class _FeedScreenState extends ConsumerState { _toggleFavorite(ref, post, favoriteKeys), onAddToCollection: (post) => _addToCollection(context, ref, post), + onHide: (post) => _hidePost(context, ref, post), ), ), ), @@ -400,6 +401,30 @@ class _FeedScreenState extends ConsumerState { ref.invalidate(favoriteKeysProvider); ref.invalidate(favoritesControllerProvider); } + + Future _hidePost( + BuildContext context, + WidgetRef ref, + Post post, + ) async { + await ref.read(feedControllerProvider.notifier).hidePost(post); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Post hidden locally'), + action: SnackBarAction( + label: 'Undo', + onPressed: () async { + await ref + .read(settingsServiceProvider) + .unhidePostKey(post.cacheKey); + ref.invalidate(appSettingsProvider); + ref.read(feedControllerProvider.notifier).refresh(); + }, + ), + ), + ); + } } class _RefreshIntent extends Intent { diff --git a/lib/features/post/presentation/post_details_screen.dart b/lib/features/post/presentation/post_details_screen.dart index 229957c..5d54b40 100644 --- a/lib/features/post/presentation/post_details_screen.dart +++ b/lib/features/post/presentation/post_details_screen.dart @@ -218,6 +218,7 @@ class PostDetailsScreen extends ConsumerWidget { onCopy: () => Clipboard.setData(ClipboardData(text: post.fileUrl)), onSimilar: () => _openSimilar(context, ref, post), + onHide: () => _hidePost(context, ref, post), onDownload: settings.allowDownloads ? () => _download(context, ref, post) : null, @@ -300,6 +301,7 @@ class PostDetailsScreen extends ConsumerWidget { onOpen: () => launchUrl(Uri.parse(post.fileUrl)), onCopy: () => Clipboard.setData(ClipboardData(text: post.fileUrl)), onSimilar: () => _openSimilar(context, ref, post), + onHide: () => _hidePost(context, ref, post), onDownload: settings.allowDownloads ? () => _download(context, ref, post) : null, @@ -442,6 +444,21 @@ class PostDetailsScreen extends ConsumerWidget { ref.invalidate(favoritesControllerProvider); } + Future _hidePost( + BuildContext context, + WidgetRef ref, + Post post, + ) async { + await ref.read(settingsServiceProvider).hidePostKey(post.cacheKey); + ref.invalidate(appSettingsProvider); + ref.read(feedControllerProvider.notifier).refresh(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Post hidden locally')), + ); + _close(context); + } + void _openPost(BuildContext context, Post post) { _replacePost(context, post); } diff --git a/lib/features/post/presentation/widgets/post_action_bar.dart b/lib/features/post/presentation/widgets/post_action_bar.dart index 2126d58..23dfc06 100644 --- a/lib/features/post/presentation/widgets/post_action_bar.dart +++ b/lib/features/post/presentation/widgets/post_action_bar.dart @@ -9,6 +9,7 @@ class PostActionBar extends StatelessWidget { required this.isFavorite, this.onDownload, this.onSimilar, + this.onHide, super.key, }); @@ -19,6 +20,7 @@ class PostActionBar extends StatelessWidget { final bool isFavorite; final VoidCallback? onDownload; final VoidCallback? onSimilar; + final VoidCallback? onHide; @override Widget build(BuildContext context) { @@ -60,6 +62,12 @@ class PostActionBar extends StatelessWidget { onPressed: onDownload, icon: const Icon(Icons.download_rounded), ), + if (onHide != null) + IconButton.filledTonal( + tooltip: 'Hide post locally', + onPressed: onHide, + icon: const Icon(Icons.visibility_off_rounded), + ), ], ); } diff --git a/lib/features/settings/presentation/settings_controller.dart b/lib/features/settings/presentation/settings_controller.dart index 588b31f..4552912 100644 --- a/lib/features/settings/presentation/settings_controller.dart +++ b/lib/features/settings/presentation/settings_controller.dart @@ -1,7 +1,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/app.dart'; +import '../../../app/app_version.dart'; import '../../../backend/backend.dart'; +import '../../../core/utils/logger.dart'; import '../../../core/utils/result.dart'; final settingsControllerProvider = @@ -30,6 +32,34 @@ class SettingsController extends AsyncNotifier { ref.invalidate(viewedKeysProvider); } + Future clearHiddenPosts() async { + await ref.read(settingsServiceProvider).clearHiddenPosts(); + ref.invalidate(appSettingsProvider); + ref.invalidateSelf(); + } + + Future diagnosticsReport() async { + final result = + await ref.read(settingsServiceProvider).buildDiagnosticsReport( + appVersion: appDisplayVersion, + buildNumber: appBuildNumber, + ); + return result is Success ? result.data : '{}'; + } + + Future diagnosticLogs() async { + final settings = state.value ?? AppSettings.defaults; + final persistent = settings.diagnosticLogLines; + final runtime = AppLogger.lines; + return [...persistent, ...runtime].join('\n'); + } + + Future clearDiagnosticLogs() async { + AppLogger.clear(); + await ref.read(settingsServiceProvider).clearDiagnosticLogs(); + ref.invalidateSelf(); + } + Future exportJson() async { final result = await ref.read(settingsServiceProvider).exportSettingsToJson(); diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 19d2122..c95948b 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../app/app_version.dart'; +import '../../../app/changelog.dart'; import '../../../backend/backend.dart'; import '../../../core/utils/result.dart'; import '../../../shared/widgets/adaptive_scaffold.dart'; @@ -151,6 +152,15 @@ class SettingsScreen extends ConsumerWidget { label: const Text('Clear cache'), ), const SizedBox(height: 12), + FilledButton.tonalIcon( + onPressed: () => ref + .read(settingsControllerProvider.notifier) + .clearHiddenPosts(), + icon: const Icon(Icons.visibility_off_rounded), + label: Text( + 'Clear hidden posts (${settings.hiddenPostKeys.length})'), + ), + const SizedBox(height: 12), FilledButton.tonalIcon( onPressed: () => ref .read(settingsControllerProvider.notifier) @@ -186,8 +196,54 @@ class SettingsScreen extends ConsumerWidget { icon: const Icon(Icons.system_update_alt_rounded), label: const Text('Check for updates'), ), + const SizedBox(height: 12), + FilledButton.tonalIcon( + onPressed: () async { + final report = await ref + .read(settingsControllerProvider.notifier) + .diagnosticsReport(); + await Clipboard.setData(ClipboardData(text: report)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Diagnostics copied')), + ); + } + }, + icon: const Icon(Icons.bug_report_rounded), + label: const Text('Copy diagnostics report'), + ), + const SizedBox(height: 12), + FilledButton.tonalIcon( + onPressed: () async { + final logs = await ref + .read(settingsControllerProvider.notifier) + .diagnosticLogs(); + await Clipboard.setData(ClipboardData(text: logs)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Diagnostic logs copied')), + ); + } + }, + icon: const Icon(Icons.receipt_long_rounded), + label: const Text('Copy local logs'), + ), + const SizedBox(height: 12), + FilledButton.tonalIcon( + onPressed: () => ref + .read(settingsControllerProvider.notifier) + .clearDiagnosticLogs(), + icon: const Icon(Icons.delete_sweep_rounded), + label: const Text('Clear local logs'), + ), + const SizedBox(height: 12), + FilledButton.tonalIcon( + onPressed: () => _showChangelog(context), + icon: const Icon(Icons.new_releases_rounded), + label: const Text('What changed'), + ), const SizedBox(height: 24), - const Text('Version $appDisplayVersion'), + const Text('Version $appDisplayVersion ($appBuildNumber)'), ], ), ), @@ -305,6 +361,45 @@ class SettingsScreen extends ConsumerWidget { ); ref.invalidate(settingsControllerProvider); } + + Future _showChangelog(BuildContext context) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('RuleGel changelog'), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + for (final change in ruleGelChangelog) ...[ + Text( + '${change.version} - ${change.title}', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + for (final bullet in change.bullets) + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 4), + child: Text('- $bullet'), + ), + const SizedBox(height: 12), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } } class _TagListEditor extends StatefulWidget { diff --git a/lib/shared/widgets/post_card.dart b/lib/shared/widgets/post_card.dart index 0f2f7cf..d3c6a1f 100644 --- a/lib/shared/widgets/post_card.dart +++ b/lib/shared/widgets/post_card.dart @@ -17,6 +17,7 @@ class PostCard extends StatefulWidget { required this.onFavorite, this.onAddToCollection, this.onPreview, + this.onHide, this.onToggleSelected, this.selectionMode = false, this.selected = false, @@ -32,6 +33,7 @@ class PostCard extends StatefulWidget { final VoidCallback onFavorite; final VoidCallback? onAddToCollection; final VoidCallback? onPreview; + final VoidCallback? onHide; final VoidCallback? onToggleSelected; final bool selectionMode; final bool selected; @@ -95,6 +97,11 @@ class _PostCardState extends State { onTap: widget.onAddToCollection, child: const Text('Add to collection'), ), + if (widget.onHide != null) + PopupMenuItem( + onTap: widget.onHide, + child: const Text('Hide post'), + ), ], ); }, @@ -217,6 +224,15 @@ class _PostCardState extends State { onPressed: widget.onAddToCollection, icon: const Icon(Icons.add_rounded), ), + if (widget.onHide != null) ...[ + const SizedBox(width: 6), + IconButton.filledTonal( + tooltip: 'Hide post', + onPressed: widget.onHide, + icon: + const Icon(Icons.visibility_off_rounded), + ), + ], ], ), ), diff --git a/lib/shared/widgets/post_masonry_grid.dart b/lib/shared/widgets/post_masonry_grid.dart index a77dde9..86f0b0d 100644 --- a/lib/shared/widgets/post_masonry_grid.dart +++ b/lib/shared/widgets/post_masonry_grid.dart @@ -17,6 +17,7 @@ class PostMasonryGrid extends StatelessWidget { required this.onFavorite, this.onAddToCollection, this.onPreview, + this.onHide, this.onToggleSelected, this.selectionMode = false, this.selectedKeys = const {}, @@ -36,6 +37,7 @@ class PostMasonryGrid extends StatelessWidget { final ValueChanged onFavorite; final ValueChanged? onAddToCollection; final ValueChanged? onPreview; + final ValueChanged? onHide; final ValueChanged? onToggleSelected; final bool selectionMode; final Set selectedKeys; @@ -66,6 +68,7 @@ class PostMasonryGrid extends StatelessWidget { onOpen: () => onOpen(post), onFavorite: () => onFavorite(post), onPreview: onPreview == null ? null : () => onPreview!(post), + onHide: onHide == null ? null : () => onHide!(post), selectionMode: selectionMode, selected: selectedKeys.contains(post.cacheKey), onToggleSelected: diff --git a/pubspec.yaml b/pubspec.yaml index 353a5ff..499930f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: gel_rule_app description: Local backend/core layer for a Flutter booru browser app. publish_to: "none" -version: 0.4.3+7 +version: 1.0.0+8 environment: sdk: ">=3.3.0 <4.0.0" diff --git a/test/backend/feed_tag_filter_test.dart b/test/backend/feed_tag_filter_test.dart index c625926..8247d9c 100644 --- a/test/backend/feed_tag_filter_test.dart +++ b/test/backend/feed_tag_filter_test.dart @@ -49,6 +49,15 @@ void main() { expect(postMatchesRequestedTags(post, ['touhou']), isTrue); expect(postMatchesRequestedTags(post, ['touhou', 'cirno']), isFalse); }); + + test('hidden post keys are filtered locally', () { + final post = _post(tags: ['touhou']); + final settings = AppSettings.defaults.copyWith( + hiddenPostKeys: [post.cacheKey], + ); + + expect(postPassesTagFilters(post, settings), isFalse); + }); }); } diff --git a/test/backend/isar_integration_test.dart b/test/backend/isar_integration_test.dart index 6c0b7b6..698ee9c 100644 --- a/test/backend/isar_integration_test.dart +++ b/test/backend/isar_integration_test.dart @@ -167,6 +167,7 @@ void main() { blacklistedTags: ['blocked'], whitelistedTags: ['allowed'], smartBlacklistRules: ['provider:e621 score:<10'], + hiddenPostKeys: ['provider:post'], ), ); @@ -178,6 +179,8 @@ void main() { expect(exported.data, contains('"allowed"')); expect(exported.data, contains('"smartBlacklistRules"')); expect(exported.data, contains('provider:e621 score:<10')); + expect(exported.data, contains('"hiddenPostKeys"')); + expect(exported.data, contains('"provider:post"')); }); test('cache service prunes to max item count', () async {