Add viewed history, diagnostics, tag chips and SWF viewer

This commit is contained in:
2026-05-24 11:35:43 +03:00
parent b02c776628
commit 361d0b3910
49 changed files with 4956 additions and 446 deletions
+69 -6
View File
@@ -3,14 +3,22 @@ import '../../core/utils/result.dart';
import '../models/post.dart';
import '../models/top_period_filter.dart';
import '../providers/provider_manager.dart';
import '../services/viewed_history_service.dart';
import '../utils/smart_blacklist.dart';
import 'settings_service.dart';
class FeedService {
FeedService(this._providerManager, this._cacheService, this._settingsService);
FeedService(
this._providerManager,
this._cacheService,
this._settingsService,
this._viewedHistoryService,
);
final ProviderManager _providerManager;
final CacheService _cacheService;
final SettingsService _settingsService;
final ViewedHistoryService _viewedHistoryService;
final Map<String, int> _pages = {};
String _key(
@@ -61,10 +69,65 @@ class FeedService {
final posts = (result as Success<List<Post>>).data;
_pages[key] = page + 1;
final settingsResult = await _settingsService.getSettings();
final maxItems = settingsResult is Success<AppSettings>
? settingsResult.data.cacheMaxItems
: null;
await _cacheService.cachePosts(posts, maxItems: maxItems);
return Success(posts);
final settings = settingsResult is Success<AppSettings>
? settingsResult.data
: AppSettings.defaults;
final viewedKeysResult = settings.hideViewedPosts
? await _viewedHistoryService.getViewedKeys()
: const Success(<String>{});
final viewedKeys = viewedKeysResult is Success<Set<String>>
? viewedKeysResult.data
: <String>{};
final filteredPosts = posts
.where((post) => postMatchesRequestedTags(post, tags))
.where((post) => postPassesTagFilters(post, settings))
.where((post) =>
!settings.hideViewedPosts || !viewedKeys.contains(post.cacheKey))
.toList(growable: false);
await _cacheService.cachePosts(
filteredPosts,
maxItems: settings.cacheMaxItems,
);
return Success(filteredPosts);
}
}
bool postMatchesRequestedTags(Post post, List<String> requestedTags) {
final requested = requestedTags
.map((tag) => tag.trim().toLowerCase())
.where((tag) => tag.isNotEmpty && !tag.startsWith('-'))
.toList(growable: false);
if (requested.isEmpty) return true;
final postTags = _postTagSet(post);
return requested.every(postTags.contains);
}
bool postPassesTagFilters(Post post, AppSettings settings) {
final tags = _postTagSet(post);
if (tags.isEmpty) return settings.whitelistedTags.isEmpty;
final blacklist = [
...settings.smartBlacklistRules,
...settings.blacklistedTags,
].map((tag) => tag.trim().toLowerCase()).where((tag) => tag.isNotEmpty);
if (blacklist.any((rule) => SmartBlacklistMatcher.matches(post, rule))) {
return false;
}
final whitelist = settings.whitelistedTags
.map((tag) => tag.trim().toLowerCase())
.where((tag) => tag.isNotEmpty)
.toList(growable: false);
if (whitelist.isEmpty) return true;
return whitelist.any(tags.contains);
}
Set<String> _postTagSet(Post post) {
return <String>{
...post.tags,
for (final group in post.tagGroups.values) ...group,
}
.map((tag) => tag.trim().toLowerCase())
.where((tag) => tag.isNotEmpty)
.toSet();
}
@@ -24,6 +24,10 @@ class AppSettings {
required this.selectedFeedProviderIds,
required this.showPostBadges,
required this.defaultTopPeriodFilter,
required this.blacklistedTags,
required this.whitelistedTags,
required this.smartBlacklistRules,
required this.hideViewedPosts,
this.defaultRatingFilter,
});
@@ -40,6 +44,10 @@ class AppSettings {
final List<String> selectedFeedProviderIds;
final bool showPostBadges;
final String defaultTopPeriodFilter;
final List<String> blacklistedTags;
final List<String> whitelistedTags;
final List<String> smartBlacklistRules;
final bool hideViewedPosts;
final String? defaultRatingFilter;
static const defaults = AppSettings(
@@ -56,6 +64,10 @@ class AppSettings {
selectedFeedProviderIds: [],
showPostBadges: true,
defaultTopPeriodFilter: 'none',
blacklistedTags: [],
whitelistedTags: [],
smartBlacklistRules: [],
hideViewedPosts: false,
defaultRatingFilter: null,
);
@@ -73,6 +85,10 @@ class AppSettings {
List<String>? selectedFeedProviderIds,
bool? showPostBadges,
String? defaultTopPeriodFilter,
List<String>? blacklistedTags,
List<String>? whitelistedTags,
List<String>? smartBlacklistRules,
bool? hideViewedPosts,
String? defaultRatingFilter,
}) {
return AppSettings(
@@ -91,6 +107,10 @@ class AppSettings {
showPostBadges: showPostBadges ?? this.showPostBadges,
defaultTopPeriodFilter:
defaultTopPeriodFilter ?? this.defaultTopPeriodFilter,
blacklistedTags: blacklistedTags ?? this.blacklistedTags,
whitelistedTags: whitelistedTags ?? this.whitelistedTags,
smartBlacklistRules: smartBlacklistRules ?? this.smartBlacklistRules,
hideViewedPosts: hideViewedPosts ?? this.hideViewedPosts,
defaultRatingFilter: defaultRatingFilter ?? this.defaultRatingFilter,
);
}
@@ -109,6 +129,10 @@ class AppSettings {
'selectedFeedProviderIds': selectedFeedProviderIds,
'showPostBadges': showPostBadges,
'defaultTopPeriodFilter': defaultTopPeriodFilter,
'blacklistedTags': blacklistedTags,
'whitelistedTags': whitelistedTags,
'smartBlacklistRules': smartBlacklistRules,
'hideViewedPosts': hideViewedPosts,
'defaultRatingFilter': defaultRatingFilter,
};
@@ -141,6 +165,19 @@ class AppSettings {
(json['showPostBadges'] as bool?) ?? defaults.showPostBadges,
defaultTopPeriodFilter: (json['defaultTopPeriodFilter'] as String?) ??
defaults.defaultTopPeriodFilter,
blacklistedTags: List<String>.from(
(json['blacklistedTags'] as List?) ?? defaults.blacklistedTags,
),
whitelistedTags: List<String>.from(
(json['whitelistedTags'] as List?) ?? defaults.whitelistedTags,
),
smartBlacklistRules: List<String>.from(
(json['smartBlacklistRules'] as List?) ??
(json['blacklistedTags'] as List?) ??
defaults.smartBlacklistRules,
),
hideViewedPosts:
(json['hideViewedPosts'] as bool?) ?? defaults.hideViewedPosts,
defaultRatingFilter: json['defaultRatingFilter'] as String?,
);
}
@@ -0,0 +1,41 @@
import '../../core/utils/result.dart';
import '../models/post.dart';
import '../models/viewed_post.dart';
import '../repositories/post_repository.dart';
import '../repositories/viewed_post_repository.dart';
class ViewedHistoryService {
ViewedHistoryService(this._repository, this._postRepository);
final ViewedPostRepository _repository;
final PostRepository _postRepository;
Future<Result<void>> markViewed(Post post) async {
final cacheResult = await _postRepository.cachePosts([post]);
if (cacheResult is Error<void>) return cacheResult;
return _repository.markViewed(post.providerId, post.id);
}
Future<Result<bool>> isViewed(String postId, String providerId) {
return _repository.exists(providerId, postId);
}
Future<Result<Set<String>>> getViewedKeys() {
return _repository.keys();
}
Future<Result<List<Post>>> getViewedPosts() async {
final viewedResult = await _repository.recent();
if (viewedResult is Error<List<ViewedPost>>) {
return Error(viewedResult.failure);
}
final viewed = (viewedResult as Success<List<ViewedPost>>).data;
return _postRepository.getCachedPostsByKeys(
viewed.map((item) => item.viewedKey),
);
}
Future<Result<void>> clearHistory() {
return _repository.clear();
}
}