Add QoL feed, download and quality improvements
This commit is contained in:
@@ -56,6 +56,13 @@ class CollectionService {
|
||||
return _repository.addPost(collectionId, post);
|
||||
}
|
||||
|
||||
Future<Result<void>> addPostsToCollection(
|
||||
String collectionId,
|
||||
List<Post> posts,
|
||||
) {
|
||||
return _repository.addPosts(collectionId, posts);
|
||||
}
|
||||
|
||||
Future<Result<void>> removePostFromCollection(
|
||||
String collectionId,
|
||||
String postId,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../models/download_task.dart';
|
||||
import '../models/post.dart';
|
||||
import '../utils/media_quality.dart';
|
||||
import 'download_service.dart';
|
||||
|
||||
class DownloadManagerService {
|
||||
DownloadManagerService(this._downloadService);
|
||||
|
||||
final DownloadService _downloadService;
|
||||
final _controller = StreamController<List<DownloadTask>>.broadcast();
|
||||
final Map<String, DownloadTask> _tasks = {};
|
||||
|
||||
Stream<List<DownloadTask>> get stream => _controller.stream;
|
||||
List<DownloadTask> get tasks => List.unmodifiable(_tasks.values);
|
||||
|
||||
Future<DownloadTask> start(Post post) async {
|
||||
final id = '${post.cacheKey}:${DateTime.now().microsecondsSinceEpoch}';
|
||||
final task = DownloadTask(
|
||||
id: id,
|
||||
post: post,
|
||||
fileName: _suggestedFileName(post),
|
||||
progress: 0,
|
||||
status: DownloadTaskStatus.queued,
|
||||
);
|
||||
_set(task);
|
||||
unawaited(_run(task));
|
||||
return task;
|
||||
}
|
||||
|
||||
Future<void> retry(String taskId) async {
|
||||
final existing = _tasks[taskId];
|
||||
if (existing == null) return;
|
||||
final task = existing.copyWith(
|
||||
progress: 0,
|
||||
status: DownloadTaskStatus.queued,
|
||||
error: null,
|
||||
);
|
||||
_set(task);
|
||||
await _run(task);
|
||||
}
|
||||
|
||||
void cancel(String taskId) {
|
||||
final existing = _tasks[taskId];
|
||||
if (existing == null) return;
|
||||
_set(existing.copyWith(status: DownloadTaskStatus.canceled));
|
||||
}
|
||||
|
||||
Future<void> _run(DownloadTask task) async {
|
||||
if (_tasks[task.id]?.status == DownloadTaskStatus.canceled) return;
|
||||
_set(task.copyWith(status: DownloadTaskStatus.running));
|
||||
try {
|
||||
final saved = await _downloadService.downloadPost(
|
||||
task.post,
|
||||
onProgress: (received, total) {
|
||||
if (total <= 0) return;
|
||||
final current = _tasks[task.id];
|
||||
if (current == null ||
|
||||
current.status == DownloadTaskStatus.canceled) {
|
||||
return;
|
||||
}
|
||||
_set(current.copyWith(progress: received / total));
|
||||
},
|
||||
);
|
||||
final current = _tasks[task.id];
|
||||
if (current == null || current.status == DownloadTaskStatus.canceled) {
|
||||
return;
|
||||
}
|
||||
_set(
|
||||
current.copyWith(
|
||||
progress: 1,
|
||||
status: DownloadTaskStatus.completed,
|
||||
savedPath: saved,
|
||||
),
|
||||
);
|
||||
_scheduleAutoRemove(task.id, const Duration(seconds: 6));
|
||||
} catch (error) {
|
||||
final current = _tasks[task.id];
|
||||
if (current == null || current.status == DownloadTaskStatus.canceled) {
|
||||
return;
|
||||
}
|
||||
_set(
|
||||
current.copyWith(
|
||||
status: DownloadTaskStatus.failed,
|
||||
error: error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _set(DownloadTask task) {
|
||||
_tasks[task.id] = task;
|
||||
_controller.add(tasks);
|
||||
}
|
||||
|
||||
void _scheduleAutoRemove(String taskId, Duration delay) {
|
||||
Future<void>.delayed(delay, () {
|
||||
final current = _tasks[taskId];
|
||||
if (current?.status != DownloadTaskStatus.completed) return;
|
||||
_tasks.remove(taskId);
|
||||
_controller.add(tasks);
|
||||
});
|
||||
}
|
||||
|
||||
String _suggestedFileName(Post post) {
|
||||
final url = MediaUrlSelector.download(post) ?? post.fileUrl;
|
||||
final parsed = Uri.tryParse(url);
|
||||
final last =
|
||||
parsed?.pathSegments.isEmpty ?? true ? '' : parsed!.pathSegments.last;
|
||||
if (last.contains('.')) return last;
|
||||
return '${post.providerId}_${post.id}';
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../models/post.dart';
|
||||
import '../utils/media_quality.dart';
|
||||
|
||||
class DownloadService {
|
||||
DownloadService({Dio? dio}) : _dio = dio ?? Dio();
|
||||
@@ -43,10 +44,7 @@ class DownloadService {
|
||||
}
|
||||
|
||||
String? _downloadUrl(Post post) {
|
||||
for (final url in [post.sampleUrl, post.fileUrl, post.previewUrl]) {
|
||||
if (url.trim().isNotEmpty) return url;
|
||||
}
|
||||
return null;
|
||||
return MediaUrlSelector.download(post);
|
||||
}
|
||||
|
||||
String _fileName(Post post, String url) {
|
||||
|
||||
@@ -78,7 +78,7 @@ class FeedService {
|
||||
final viewedKeys = viewedKeysResult is Success<Set<String>>
|
||||
? viewedKeysResult.data
|
||||
: <String>{};
|
||||
final filteredPosts = posts
|
||||
final filteredPosts = _applyTopPeriod(posts, topPeriod)
|
||||
.where((post) => postMatchesRequestedTags(post, tags))
|
||||
.where((post) => postPassesTagFilters(post, settings))
|
||||
.where((post) =>
|
||||
@@ -90,6 +90,25 @@ class FeedService {
|
||||
);
|
||||
return Success(filteredPosts);
|
||||
}
|
||||
|
||||
List<Post> _applyTopPeriod(List<Post> posts, TopPeriodFilter period) {
|
||||
if (period == TopPeriodFilter.none) return posts;
|
||||
|
||||
final sorted = [...posts]..sort((a, b) => b.score.compareTo(a.score));
|
||||
if (period == TopPeriodFilter.allTime) return sorted;
|
||||
|
||||
final now = DateTime.now();
|
||||
final minDate = period == TopPeriodFilter.month
|
||||
? now.subtract(const Duration(days: 31))
|
||||
: DateTime(now.year - 1, now.month, now.day);
|
||||
final inPeriod = sorted
|
||||
.where((post) =>
|
||||
post.createdAt.isAfter(minDate) &&
|
||||
post.createdAt.isBefore(now.add(const Duration(days: 1))))
|
||||
.toList(growable: false);
|
||||
|
||||
return inPeriod.isEmpty ? sorted : inPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
bool postMatchesRequestedTags(Post post, List<String> requestedTags) {
|
||||
|
||||
@@ -28,7 +28,13 @@ class AppSettings {
|
||||
required this.whitelistedTags,
|
||||
required this.smartBlacklistRules,
|
||||
required this.hideViewedPosts,
|
||||
required this.mediaQualityMode,
|
||||
required this.lastFeedTags,
|
||||
required this.lastFeedProviderIds,
|
||||
required this.lastFeedTopPeriod,
|
||||
required this.lastFeedScrollOffset,
|
||||
this.defaultRatingFilter,
|
||||
this.lastFeedRating,
|
||||
});
|
||||
|
||||
final List<String> enabledProviderIds;
|
||||
@@ -48,7 +54,13 @@ class AppSettings {
|
||||
final List<String> whitelistedTags;
|
||||
final List<String> smartBlacklistRules;
|
||||
final bool hideViewedPosts;
|
||||
final String mediaQualityMode;
|
||||
final List<String> lastFeedTags;
|
||||
final List<String> lastFeedProviderIds;
|
||||
final String lastFeedTopPeriod;
|
||||
final double lastFeedScrollOffset;
|
||||
final String? defaultRatingFilter;
|
||||
final String? lastFeedRating;
|
||||
|
||||
static const defaults = AppSettings(
|
||||
enabledProviderIds: ['gelbooru', 'rule34', 'safebooru'],
|
||||
@@ -68,7 +80,13 @@ class AppSettings {
|
||||
whitelistedTags: [],
|
||||
smartBlacklistRules: [],
|
||||
hideViewedPosts: false,
|
||||
mediaQualityMode: 'auto',
|
||||
lastFeedTags: [],
|
||||
lastFeedProviderIds: [],
|
||||
lastFeedTopPeriod: 'none',
|
||||
lastFeedScrollOffset: 0,
|
||||
defaultRatingFilter: null,
|
||||
lastFeedRating: null,
|
||||
);
|
||||
|
||||
AppSettings copyWith({
|
||||
@@ -89,7 +107,14 @@ class AppSettings {
|
||||
List<String>? whitelistedTags,
|
||||
List<String>? smartBlacklistRules,
|
||||
bool? hideViewedPosts,
|
||||
String? mediaQualityMode,
|
||||
List<String>? lastFeedTags,
|
||||
List<String>? lastFeedProviderIds,
|
||||
String? lastFeedTopPeriod,
|
||||
double? lastFeedScrollOffset,
|
||||
String? defaultRatingFilter,
|
||||
String? lastFeedRating,
|
||||
bool clearLastFeedRating = false,
|
||||
}) {
|
||||
return AppSettings(
|
||||
enabledProviderIds: enabledProviderIds ?? this.enabledProviderIds,
|
||||
@@ -111,7 +136,14 @@ class AppSettings {
|
||||
whitelistedTags: whitelistedTags ?? this.whitelistedTags,
|
||||
smartBlacklistRules: smartBlacklistRules ?? this.smartBlacklistRules,
|
||||
hideViewedPosts: hideViewedPosts ?? this.hideViewedPosts,
|
||||
mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode,
|
||||
lastFeedTags: lastFeedTags ?? this.lastFeedTags,
|
||||
lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds,
|
||||
lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod,
|
||||
lastFeedScrollOffset: lastFeedScrollOffset ?? this.lastFeedScrollOffset,
|
||||
defaultRatingFilter: defaultRatingFilter ?? this.defaultRatingFilter,
|
||||
lastFeedRating:
|
||||
clearLastFeedRating ? null : lastFeedRating ?? this.lastFeedRating,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +165,13 @@ class AppSettings {
|
||||
'whitelistedTags': whitelistedTags,
|
||||
'smartBlacklistRules': smartBlacklistRules,
|
||||
'hideViewedPosts': hideViewedPosts,
|
||||
'mediaQualityMode': mediaQualityMode,
|
||||
'lastFeedTags': lastFeedTags,
|
||||
'lastFeedProviderIds': lastFeedProviderIds,
|
||||
'lastFeedTopPeriod': lastFeedTopPeriod,
|
||||
'lastFeedScrollOffset': lastFeedScrollOffset,
|
||||
'defaultRatingFilter': defaultRatingFilter,
|
||||
'lastFeedRating': lastFeedRating,
|
||||
};
|
||||
|
||||
factory AppSettings.fromJson(Map<String, dynamic> json) => AppSettings(
|
||||
@@ -178,7 +216,24 @@ class AppSettings {
|
||||
),
|
||||
hideViewedPosts:
|
||||
(json['hideViewedPosts'] as bool?) ?? defaults.hideViewedPosts,
|
||||
mediaQualityMode:
|
||||
(json['mediaQualityMode'] as String?) ?? defaults.mediaQualityMode,
|
||||
lastFeedTags: List<String>.from(
|
||||
(json['lastFeedTags'] as List?) ?? defaults.lastFeedTags,
|
||||
),
|
||||
lastFeedProviderIds: List<String>.from(
|
||||
(json['lastFeedProviderIds'] as List?) ??
|
||||
(json['selectedFeedProviderIds'] as List?) ??
|
||||
defaults.lastFeedProviderIds,
|
||||
),
|
||||
lastFeedTopPeriod: (json['lastFeedTopPeriod'] as String?) ??
|
||||
(json['defaultTopPeriodFilter'] as String?) ??
|
||||
defaults.lastFeedTopPeriod,
|
||||
lastFeedScrollOffset:
|
||||
(json['lastFeedScrollOffset'] as num?)?.toDouble() ??
|
||||
defaults.lastFeedScrollOffset,
|
||||
defaultRatingFilter: json['defaultRatingFilter'] as String?,
|
||||
lastFeedRating: json['lastFeedRating'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user