Prepare RuleGel 1.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "RuleGel"
|
#define MyAppName "RuleGel"
|
||||||
#define MyAppVersion "0.4.3"
|
#define MyAppVersion "1.0.0"
|
||||||
#define MyAppPublisher "RarDog"
|
#define MyAppPublisher "RarDog"
|
||||||
#define MyAppExeName "gel_rule_app.exe"
|
#define MyAppExeName "gel_rule_app.exe"
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
const appDisplayVersion = '0.4.3';
|
const appDisplayVersion = '1.0.0';
|
||||||
const appBuildNumber = 7;
|
const appBuildNumber = 8;
|
||||||
|
|||||||
@@ -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<String> bullets;
|
||||||
|
}
|
||||||
@@ -168,7 +168,7 @@ final updateServiceProvider = Provider<UpdateService>((ref) {
|
|||||||
connectTimeout: const Duration(seconds: 12),
|
connectTimeout: const Duration(seconds: 12),
|
||||||
receiveTimeout: const Duration(seconds: 12),
|
receiveTimeout: const Duration(seconds: 12),
|
||||||
headers: const {
|
headers: const {
|
||||||
'User-Agent': 'RuleGel/0.4 Flutter local booru browser',
|
'User-Agent': 'RuleGel/1.0 Flutter local booru browser',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ bool postMatchesRequestedTags(Post post, List<String> requestedTags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool postPassesTagFilters(Post post, AppSettings settings) {
|
bool postPassesTagFilters(Post post, AppSettings settings) {
|
||||||
|
if (settings.hiddenPostKeys.contains(post.cacheKey)) return false;
|
||||||
|
|
||||||
final tags = _postTagSet(post);
|
final tags = _postTagSet(post);
|
||||||
if (tags.isEmpty) return settings.whitelistedTags.isEmpty;
|
if (tags.isEmpty) return settings.whitelistedTags.isEmpty;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../core/database/database_service.dart';
|
|||||||
import '../../core/errors/failure.dart';
|
import '../../core/errors/failure.dart';
|
||||||
import '../../core/utils/result.dart';
|
import '../../core/utils/result.dart';
|
||||||
import '../models/content_provider_config.dart';
|
import '../models/content_provider_config.dart';
|
||||||
|
import '../models/provider_diagnostics.dart';
|
||||||
import '../repositories/provider_repository.dart';
|
import '../repositories/provider_repository.dart';
|
||||||
|
|
||||||
class AppSettings {
|
class AppSettings {
|
||||||
@@ -29,6 +30,8 @@ class AppSettings {
|
|||||||
required this.smartBlacklistRules,
|
required this.smartBlacklistRules,
|
||||||
required this.hideViewedPosts,
|
required this.hideViewedPosts,
|
||||||
required this.mediaQualityMode,
|
required this.mediaQualityMode,
|
||||||
|
required this.hiddenPostKeys,
|
||||||
|
required this.diagnosticLogLines,
|
||||||
required this.lastFeedTags,
|
required this.lastFeedTags,
|
||||||
required this.lastFeedProviderIds,
|
required this.lastFeedProviderIds,
|
||||||
required this.lastFeedTopPeriod,
|
required this.lastFeedTopPeriod,
|
||||||
@@ -57,6 +60,8 @@ class AppSettings {
|
|||||||
final List<String> smartBlacklistRules;
|
final List<String> smartBlacklistRules;
|
||||||
final bool hideViewedPosts;
|
final bool hideViewedPosts;
|
||||||
final String mediaQualityMode;
|
final String mediaQualityMode;
|
||||||
|
final List<String> hiddenPostKeys;
|
||||||
|
final List<String> diagnosticLogLines;
|
||||||
final List<String> lastFeedTags;
|
final List<String> lastFeedTags;
|
||||||
final List<String> lastFeedProviderIds;
|
final List<String> lastFeedProviderIds;
|
||||||
final String lastFeedTopPeriod;
|
final String lastFeedTopPeriod;
|
||||||
@@ -85,6 +90,8 @@ class AppSettings {
|
|||||||
smartBlacklistRules: [],
|
smartBlacklistRules: [],
|
||||||
hideViewedPosts: false,
|
hideViewedPosts: false,
|
||||||
mediaQualityMode: 'auto',
|
mediaQualityMode: 'auto',
|
||||||
|
hiddenPostKeys: [],
|
||||||
|
diagnosticLogLines: [],
|
||||||
lastFeedTags: [],
|
lastFeedTags: [],
|
||||||
lastFeedProviderIds: [],
|
lastFeedProviderIds: [],
|
||||||
lastFeedTopPeriod: 'none',
|
lastFeedTopPeriod: 'none',
|
||||||
@@ -114,6 +121,8 @@ class AppSettings {
|
|||||||
List<String>? smartBlacklistRules,
|
List<String>? smartBlacklistRules,
|
||||||
bool? hideViewedPosts,
|
bool? hideViewedPosts,
|
||||||
String? mediaQualityMode,
|
String? mediaQualityMode,
|
||||||
|
List<String>? hiddenPostKeys,
|
||||||
|
List<String>? diagnosticLogLines,
|
||||||
List<String>? lastFeedTags,
|
List<String>? lastFeedTags,
|
||||||
List<String>? lastFeedProviderIds,
|
List<String>? lastFeedProviderIds,
|
||||||
String? lastFeedTopPeriod,
|
String? lastFeedTopPeriod,
|
||||||
@@ -145,6 +154,8 @@ class AppSettings {
|
|||||||
smartBlacklistRules: smartBlacklistRules ?? this.smartBlacklistRules,
|
smartBlacklistRules: smartBlacklistRules ?? this.smartBlacklistRules,
|
||||||
hideViewedPosts: hideViewedPosts ?? this.hideViewedPosts,
|
hideViewedPosts: hideViewedPosts ?? this.hideViewedPosts,
|
||||||
mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode,
|
mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode,
|
||||||
|
hiddenPostKeys: hiddenPostKeys ?? this.hiddenPostKeys,
|
||||||
|
diagnosticLogLines: diagnosticLogLines ?? this.diagnosticLogLines,
|
||||||
lastFeedTags: lastFeedTags ?? this.lastFeedTags,
|
lastFeedTags: lastFeedTags ?? this.lastFeedTags,
|
||||||
lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds,
|
lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds,
|
||||||
lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod,
|
lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod,
|
||||||
@@ -176,6 +187,8 @@ class AppSettings {
|
|||||||
'smartBlacklistRules': smartBlacklistRules,
|
'smartBlacklistRules': smartBlacklistRules,
|
||||||
'hideViewedPosts': hideViewedPosts,
|
'hideViewedPosts': hideViewedPosts,
|
||||||
'mediaQualityMode': mediaQualityMode,
|
'mediaQualityMode': mediaQualityMode,
|
||||||
|
'hiddenPostKeys': hiddenPostKeys,
|
||||||
|
'diagnosticLogLines': diagnosticLogLines,
|
||||||
'lastFeedTags': lastFeedTags,
|
'lastFeedTags': lastFeedTags,
|
||||||
'lastFeedProviderIds': lastFeedProviderIds,
|
'lastFeedProviderIds': lastFeedProviderIds,
|
||||||
'lastFeedTopPeriod': lastFeedTopPeriod,
|
'lastFeedTopPeriod': lastFeedTopPeriod,
|
||||||
@@ -230,6 +243,12 @@ class AppSettings {
|
|||||||
(json['hideViewedPosts'] as bool?) ?? defaults.hideViewedPosts,
|
(json['hideViewedPosts'] as bool?) ?? defaults.hideViewedPosts,
|
||||||
mediaQualityMode:
|
mediaQualityMode:
|
||||||
(json['mediaQualityMode'] as String?) ?? defaults.mediaQualityMode,
|
(json['mediaQualityMode'] as String?) ?? defaults.mediaQualityMode,
|
||||||
|
hiddenPostKeys: List<String>.from(
|
||||||
|
(json['hiddenPostKeys'] as List?) ?? defaults.hiddenPostKeys,
|
||||||
|
),
|
||||||
|
diagnosticLogLines: List<String>.from(
|
||||||
|
(json['diagnosticLogLines'] as List?) ?? defaults.diagnosticLogLines,
|
||||||
|
),
|
||||||
lastFeedTags: List<String>.from(
|
lastFeedTags: List<String>.from(
|
||||||
(json['lastFeedTags'] as List?) ?? defaults.lastFeedTags,
|
(json['lastFeedTags'] as List?) ?? defaults.lastFeedTags,
|
||||||
),
|
),
|
||||||
@@ -331,6 +350,124 @@ class SettingsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> hidePostKey(String cacheKey) async {
|
||||||
|
final result = await getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
final hidden = <String>{...settings.hiddenPostKeys, cacheKey}.toList()
|
||||||
|
..sort();
|
||||||
|
return updateSettings(settings.copyWith(hiddenPostKeys: hidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> unhidePostKey(String cacheKey) async {
|
||||||
|
final result = await getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
return updateSettings(
|
||||||
|
settings.copyWith(
|
||||||
|
hiddenPostKeys:
|
||||||
|
settings.hiddenPostKeys.where((key) => key != cacheKey).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> clearHiddenPosts() async {
|
||||||
|
final result = await getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
return updateSettings(settings.copyWith(hiddenPostKeys: const []));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> appendDiagnosticLog(String message) async {
|
||||||
|
final result = await getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).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<Result<void>> clearDiagnosticLogs() async {
|
||||||
|
final result = await getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
return updateSettings(settings.copyWith(diagnosticLogLines: const []));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<String>> buildDiagnosticsReport({
|
||||||
|
required String appVersion,
|
||||||
|
required int buildNumber,
|
||||||
|
}) async {
|
||||||
|
final settingsResult = await getSettings();
|
||||||
|
if (settingsResult is Error<AppSettings>) {
|
||||||
|
return Error(settingsResult.failure);
|
||||||
|
}
|
||||||
|
final settings = (settingsResult as Success<AppSettings>).data;
|
||||||
|
final providers = <ContentProviderConfig>[];
|
||||||
|
final diagnostics = <String>[];
|
||||||
|
final repository = _providerRepository;
|
||||||
|
if (repository != null) {
|
||||||
|
final providersResult = await repository.getProviders(enabledOnly: false);
|
||||||
|
if (providersResult is Success<List<ContentProviderConfig>>) {
|
||||||
|
providers.addAll(providersResult.data);
|
||||||
|
}
|
||||||
|
final diagnosticsResult = await repository.getDiagnostics();
|
||||||
|
if (diagnosticsResult is Success<List<ProviderDiagnostics>>) {
|
||||||
|
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<Result<String>> exportSettingsToJson() async {
|
Future<Result<String>> exportSettingsToJson() async {
|
||||||
final result = await getSettings();
|
final result = await getSettings();
|
||||||
if (result is Error<AppSettings>) return Error(result.failure);
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
@@ -351,6 +488,7 @@ class SettingsService {
|
|||||||
'blacklistedTags': settings.blacklistedTags,
|
'blacklistedTags': settings.blacklistedTags,
|
||||||
'whitelistedTags': settings.whitelistedTags,
|
'whitelistedTags': settings.whitelistedTags,
|
||||||
'smartBlacklistRules': settings.smartBlacklistRules,
|
'smartBlacklistRules': settings.smartBlacklistRules,
|
||||||
|
'hiddenPostKeys': settings.hiddenPostKeys,
|
||||||
},
|
},
|
||||||
'providers': providers.map((provider) => provider.toJson()).toList(),
|
'providers': providers.map((provider) => provider.toJson()).toList(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,7 +3,17 @@ import 'package:flutter/foundation.dart';
|
|||||||
class AppLogger {
|
class AppLogger {
|
||||||
const AppLogger();
|
const AppLogger();
|
||||||
|
|
||||||
|
static final List<String> _lines = <String>[];
|
||||||
|
|
||||||
|
static List<String> get lines => List.unmodifiable(_lines);
|
||||||
|
|
||||||
|
static void clear() => _lines.clear();
|
||||||
|
|
||||||
void debug(String message, [Object? error, StackTrace? stackTrace]) {
|
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) {
|
if (kDebugMode) {
|
||||||
debugPrint('[GelRule] $message');
|
debugPrint('[GelRule] $message');
|
||||||
if (error != null) debugPrint(' error: $error');
|
if (error != null) debugPrint(' error: $error');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../app/app.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
import 'feed_state.dart';
|
import 'feed_state.dart';
|
||||||
@@ -153,6 +154,22 @@ class FeedController extends AsyncNotifier<FeedState> {
|
|||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void>) return;
|
||||||
|
state = AsyncData(
|
||||||
|
current.copyWith(
|
||||||
|
posts: current.posts
|
||||||
|
.where((item) => item.cacheKey != post.cacheKey)
|
||||||
|
.toList(growable: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ref.invalidate(appSettingsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveSession({double? scrollOffset}) async {
|
Future<void> saveSession({double? scrollOffset}) async {
|
||||||
final current = state.value;
|
final current = state.value;
|
||||||
if (current == null) return;
|
if (current == null) return;
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
|||||||
_toggleFavorite(ref, post, favoriteKeys),
|
_toggleFavorite(ref, post, favoriteKeys),
|
||||||
onAddToCollection: (post) =>
|
onAddToCollection: (post) =>
|
||||||
_addToCollection(context, ref, post),
|
_addToCollection(context, ref, post),
|
||||||
|
onHide: (post) => _hidePost(context, ref, post),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -400,6 +401,30 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
|||||||
ref.invalidate(favoriteKeysProvider);
|
ref.invalidate(favoriteKeysProvider);
|
||||||
ref.invalidate(favoritesControllerProvider);
|
ref.invalidate(favoritesControllerProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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 {
|
class _RefreshIntent extends Intent {
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
onCopy: () =>
|
onCopy: () =>
|
||||||
Clipboard.setData(ClipboardData(text: post.fileUrl)),
|
Clipboard.setData(ClipboardData(text: post.fileUrl)),
|
||||||
onSimilar: () => _openSimilar(context, ref, post),
|
onSimilar: () => _openSimilar(context, ref, post),
|
||||||
|
onHide: () => _hidePost(context, ref, post),
|
||||||
onDownload: settings.allowDownloads
|
onDownload: settings.allowDownloads
|
||||||
? () => _download(context, ref, post)
|
? () => _download(context, ref, post)
|
||||||
: null,
|
: null,
|
||||||
@@ -300,6 +301,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
onOpen: () => launchUrl(Uri.parse(post.fileUrl)),
|
onOpen: () => launchUrl(Uri.parse(post.fileUrl)),
|
||||||
onCopy: () => Clipboard.setData(ClipboardData(text: post.fileUrl)),
|
onCopy: () => Clipboard.setData(ClipboardData(text: post.fileUrl)),
|
||||||
onSimilar: () => _openSimilar(context, ref, post),
|
onSimilar: () => _openSimilar(context, ref, post),
|
||||||
|
onHide: () => _hidePost(context, ref, post),
|
||||||
onDownload: settings.allowDownloads
|
onDownload: settings.allowDownloads
|
||||||
? () => _download(context, ref, post)
|
? () => _download(context, ref, post)
|
||||||
: null,
|
: null,
|
||||||
@@ -442,6 +444,21 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
ref.invalidate(favoritesControllerProvider);
|
ref.invalidate(favoritesControllerProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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) {
|
void _openPost(BuildContext context, Post post) {
|
||||||
_replacePost(context, post);
|
_replacePost(context, post);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PostActionBar extends StatelessWidget {
|
|||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
this.onDownload,
|
this.onDownload,
|
||||||
this.onSimilar,
|
this.onSimilar,
|
||||||
|
this.onHide,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class PostActionBar extends StatelessWidget {
|
|||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final VoidCallback? onDownload;
|
final VoidCallback? onDownload;
|
||||||
final VoidCallback? onSimilar;
|
final VoidCallback? onSimilar;
|
||||||
|
final VoidCallback? onHide;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -60,6 +62,12 @@ class PostActionBar extends StatelessWidget {
|
|||||||
onPressed: onDownload,
|
onPressed: onDownload,
|
||||||
icon: const Icon(Icons.download_rounded),
|
icon: const Icon(Icons.download_rounded),
|
||||||
),
|
),
|
||||||
|
if (onHide != null)
|
||||||
|
IconButton.filledTonal(
|
||||||
|
tooltip: 'Hide post locally',
|
||||||
|
onPressed: onHide,
|
||||||
|
icon: const Icon(Icons.visibility_off_rounded),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_version.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
|
import '../../../core/utils/logger.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
|
|
||||||
final settingsControllerProvider =
|
final settingsControllerProvider =
|
||||||
@@ -30,6 +32,34 @@ class SettingsController extends AsyncNotifier<AppSettings> {
|
|||||||
ref.invalidate(viewedKeysProvider);
|
ref.invalidate(viewedKeysProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> clearHiddenPosts() async {
|
||||||
|
await ref.read(settingsServiceProvider).clearHiddenPosts();
|
||||||
|
ref.invalidate(appSettingsProvider);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> diagnosticsReport() async {
|
||||||
|
final result =
|
||||||
|
await ref.read(settingsServiceProvider).buildDiagnosticsReport(
|
||||||
|
appVersion: appDisplayVersion,
|
||||||
|
buildNumber: appBuildNumber,
|
||||||
|
);
|
||||||
|
return result is Success<String> ? result.data : '{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> diagnosticLogs() async {
|
||||||
|
final settings = state.value ?? AppSettings.defaults;
|
||||||
|
final persistent = settings.diagnosticLogLines;
|
||||||
|
final runtime = AppLogger.lines;
|
||||||
|
return [...persistent, ...runtime].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearDiagnosticLogs() async {
|
||||||
|
AppLogger.clear();
|
||||||
|
await ref.read(settingsServiceProvider).clearDiagnosticLogs();
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> exportJson() async {
|
Future<String> exportJson() async {
|
||||||
final result =
|
final result =
|
||||||
await ref.read(settingsServiceProvider).exportSettingsToJson();
|
await ref.read(settingsServiceProvider).exportSettingsToJson();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../app/app_version.dart';
|
import '../../../app/app_version.dart';
|
||||||
|
import '../../../app/changelog.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
import '../../../shared/widgets/adaptive_scaffold.dart';
|
import '../../../shared/widgets/adaptive_scaffold.dart';
|
||||||
@@ -151,6 +152,15 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
label: const Text('Clear cache'),
|
label: const Text('Clear cache'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
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(
|
FilledButton.tonalIcon(
|
||||||
onPressed: () => ref
|
onPressed: () => ref
|
||||||
.read(settingsControllerProvider.notifier)
|
.read(settingsControllerProvider.notifier)
|
||||||
@@ -186,8 +196,54 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.system_update_alt_rounded),
|
icon: const Icon(Icons.system_update_alt_rounded),
|
||||||
label: const Text('Check for updates'),
|
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 SizedBox(height: 24),
|
||||||
const Text('Version $appDisplayVersion'),
|
const Text('Version $appDisplayVersion ($appBuildNumber)'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -305,6 +361,45 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
ref.invalidate(settingsControllerProvider);
|
ref.invalidate(settingsControllerProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showChangelog(BuildContext context) {
|
||||||
|
return showDialog<void>(
|
||||||
|
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 {
|
class _TagListEditor extends StatefulWidget {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class PostCard extends StatefulWidget {
|
|||||||
required this.onFavorite,
|
required this.onFavorite,
|
||||||
this.onAddToCollection,
|
this.onAddToCollection,
|
||||||
this.onPreview,
|
this.onPreview,
|
||||||
|
this.onHide,
|
||||||
this.onToggleSelected,
|
this.onToggleSelected,
|
||||||
this.selectionMode = false,
|
this.selectionMode = false,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
@@ -32,6 +33,7 @@ class PostCard extends StatefulWidget {
|
|||||||
final VoidCallback onFavorite;
|
final VoidCallback onFavorite;
|
||||||
final VoidCallback? onAddToCollection;
|
final VoidCallback? onAddToCollection;
|
||||||
final VoidCallback? onPreview;
|
final VoidCallback? onPreview;
|
||||||
|
final VoidCallback? onHide;
|
||||||
final VoidCallback? onToggleSelected;
|
final VoidCallback? onToggleSelected;
|
||||||
final bool selectionMode;
|
final bool selectionMode;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
@@ -95,6 +97,11 @@ class _PostCardState extends State<PostCard> {
|
|||||||
onTap: widget.onAddToCollection,
|
onTap: widget.onAddToCollection,
|
||||||
child: const Text('Add to collection'),
|
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<PostCard> {
|
|||||||
onPressed: widget.onAddToCollection,
|
onPressed: widget.onAddToCollection,
|
||||||
icon: const Icon(Icons.add_rounded),
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class PostMasonryGrid extends StatelessWidget {
|
|||||||
required this.onFavorite,
|
required this.onFavorite,
|
||||||
this.onAddToCollection,
|
this.onAddToCollection,
|
||||||
this.onPreview,
|
this.onPreview,
|
||||||
|
this.onHide,
|
||||||
this.onToggleSelected,
|
this.onToggleSelected,
|
||||||
this.selectionMode = false,
|
this.selectionMode = false,
|
||||||
this.selectedKeys = const {},
|
this.selectedKeys = const {},
|
||||||
@@ -36,6 +37,7 @@ class PostMasonryGrid extends StatelessWidget {
|
|||||||
final ValueChanged<Post> onFavorite;
|
final ValueChanged<Post> onFavorite;
|
||||||
final ValueChanged<Post>? onAddToCollection;
|
final ValueChanged<Post>? onAddToCollection;
|
||||||
final ValueChanged<Post>? onPreview;
|
final ValueChanged<Post>? onPreview;
|
||||||
|
final ValueChanged<Post>? onHide;
|
||||||
final ValueChanged<Post>? onToggleSelected;
|
final ValueChanged<Post>? onToggleSelected;
|
||||||
final bool selectionMode;
|
final bool selectionMode;
|
||||||
final Set<String> selectedKeys;
|
final Set<String> selectedKeys;
|
||||||
@@ -66,6 +68,7 @@ class PostMasonryGrid extends StatelessWidget {
|
|||||||
onOpen: () => onOpen(post),
|
onOpen: () => onOpen(post),
|
||||||
onFavorite: () => onFavorite(post),
|
onFavorite: () => onFavorite(post),
|
||||||
onPreview: onPreview == null ? null : () => onPreview!(post),
|
onPreview: onPreview == null ? null : () => onPreview!(post),
|
||||||
|
onHide: onHide == null ? null : () => onHide!(post),
|
||||||
selectionMode: selectionMode,
|
selectionMode: selectionMode,
|
||||||
selected: selectedKeys.contains(post.cacheKey),
|
selected: selectedKeys.contains(post.cacheKey),
|
||||||
onToggleSelected:
|
onToggleSelected:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 0.4.3+7
|
version: 1.0.0+8
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ void main() {
|
|||||||
expect(postMatchesRequestedTags(post, ['touhou']), isTrue);
|
expect(postMatchesRequestedTags(post, ['touhou']), isTrue);
|
||||||
expect(postMatchesRequestedTags(post, ['touhou', 'cirno']), isFalse);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ void main() {
|
|||||||
blacklistedTags: ['blocked'],
|
blacklistedTags: ['blocked'],
|
||||||
whitelistedTags: ['allowed'],
|
whitelistedTags: ['allowed'],
|
||||||
smartBlacklistRules: ['provider:e621 score:<10'],
|
smartBlacklistRules: ['provider:e621 score:<10'],
|
||||||
|
hiddenPostKeys: ['provider:post'],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,6 +179,8 @@ void main() {
|
|||||||
expect(exported.data, contains('"allowed"'));
|
expect(exported.data, contains('"allowed"'));
|
||||||
expect(exported.data, contains('"smartBlacklistRules"'));
|
expect(exported.data, contains('"smartBlacklistRules"'));
|
||||||
expect(exported.data, contains('provider:e621 score:<10'));
|
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 {
|
test('cache service prunes to max item count', () async {
|
||||||
|
|||||||
Reference in New Issue
Block a user