Add QoL feed, download and quality improvements
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
export 'di/backend_providers.dart';
|
||||
export 'models/collection.dart';
|
||||
export 'models/content_provider_config.dart';
|
||||
export 'models/download_task.dart';
|
||||
export 'models/favorite.dart';
|
||||
export 'models/post.dart';
|
||||
export 'models/post_comment.dart';
|
||||
@@ -14,6 +15,7 @@ export 'providers/content_provider.dart';
|
||||
export 'providers/provider_manager.dart';
|
||||
export 'services/collection_service.dart';
|
||||
export 'services/download_service.dart';
|
||||
export 'services/download_manager_service.dart';
|
||||
export 'services/favorite_service.dart';
|
||||
export 'services/feed_service.dart';
|
||||
export 'services/provider_check_service.dart';
|
||||
@@ -21,3 +23,4 @@ export 'services/search_service.dart';
|
||||
export 'services/settings_service.dart';
|
||||
export 'services/viewed_history_service.dart';
|
||||
export 'utils/smart_blacklist.dart';
|
||||
export 'utils/media_quality.dart';
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../core/http/network_info.dart';
|
||||
import '../../core/utils/result.dart';
|
||||
import '../providers/provider_factory.dart';
|
||||
import '../providers/provider_manager.dart';
|
||||
import '../models/download_task.dart';
|
||||
import '../repositories/collection_repository.dart';
|
||||
import '../repositories/favorite_repository.dart';
|
||||
import '../repositories/post_repository.dart';
|
||||
@@ -17,6 +18,7 @@ import '../repositories/search_repository.dart';
|
||||
import '../repositories/viewed_post_repository.dart';
|
||||
import '../services/collection_service.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../services/download_manager_service.dart';
|
||||
import '../services/favorite_service.dart';
|
||||
import '../services/feed_service.dart';
|
||||
import '../services/provider_check_service.dart';
|
||||
@@ -147,3 +149,12 @@ final settingsServiceProvider = Provider<SettingsService>((ref) {
|
||||
final downloadServiceProvider = Provider<DownloadService>((ref) {
|
||||
return DownloadService();
|
||||
});
|
||||
|
||||
final downloadManagerServiceProvider = Provider<DownloadManagerService>((ref) {
|
||||
return DownloadManagerService(ref.watch(downloadServiceProvider));
|
||||
});
|
||||
|
||||
final downloadTasksProvider = StreamProvider<List<DownloadTask>>((ref) {
|
||||
final manager = ref.watch(downloadManagerServiceProvider);
|
||||
return manager.stream;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'post.dart';
|
||||
|
||||
enum DownloadTaskStatus {
|
||||
queued,
|
||||
running,
|
||||
completed,
|
||||
failed,
|
||||
canceled,
|
||||
}
|
||||
|
||||
class DownloadTask {
|
||||
const DownloadTask({
|
||||
required this.id,
|
||||
required this.post,
|
||||
required this.fileName,
|
||||
required this.progress,
|
||||
required this.status,
|
||||
this.savedPath,
|
||||
this.error,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final Post post;
|
||||
final String fileName;
|
||||
final double progress;
|
||||
final DownloadTaskStatus status;
|
||||
final String? savedPath;
|
||||
final String? error;
|
||||
|
||||
DownloadTask copyWith({
|
||||
double? progress,
|
||||
DownloadTaskStatus? status,
|
||||
String? savedPath,
|
||||
String? error,
|
||||
}) {
|
||||
return DownloadTask(
|
||||
id: id,
|
||||
post: post,
|
||||
fileName: fileName,
|
||||
progress: progress ?? this.progress,
|
||||
status: status ?? this.status,
|
||||
savedPath: savedPath ?? this.savedPath,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -147,10 +147,24 @@ class DanbooruProvider implements ContentProvider, CommentProvider {
|
||||
}
|
||||
|
||||
List<String> _topTags(TopPeriodFilter period) {
|
||||
final now = DateTime.now();
|
||||
return switch (period) {
|
||||
TopPeriodFilter.none => const [],
|
||||
TopPeriodFilter.month || TopPeriodFilter.year => const ['order:rank'],
|
||||
TopPeriodFilter.month => [
|
||||
'order:score',
|
||||
'date:>${_date(now.subtract(const Duration(days: 31)))}',
|
||||
],
|
||||
TopPeriodFilter.year => [
|
||||
'order:score',
|
||||
'date:>${_date(DateTime(now.year - 1, now.month, now.day))}',
|
||||
],
|
||||
TopPeriodFilter.allTime => const ['order:score'],
|
||||
};
|
||||
}
|
||||
|
||||
String _date(DateTime value) {
|
||||
return '${value.year.toString().padLeft(4, '0')}-'
|
||||
'${value.month.toString().padLeft(2, '0')}-'
|
||||
'${value.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,13 +168,27 @@ class E621Provider
|
||||
}
|
||||
|
||||
List<String> _topTags(TopPeriodFilter period) {
|
||||
final now = DateTime.now();
|
||||
return switch (period) {
|
||||
TopPeriodFilter.none => const [],
|
||||
TopPeriodFilter.month => const ['order:rank'],
|
||||
TopPeriodFilter.year || TopPeriodFilter.allTime => const ['order:score'],
|
||||
TopPeriodFilter.month => [
|
||||
'order:score',
|
||||
'date:>${_date(now.subtract(const Duration(days: 31)))}',
|
||||
],
|
||||
TopPeriodFilter.year => [
|
||||
'order:score',
|
||||
'date:>${_date(DateTime(now.year - 1, now.month, now.day))}',
|
||||
],
|
||||
TopPeriodFilter.allTime => const ['order:score'],
|
||||
};
|
||||
}
|
||||
|
||||
String _date(DateTime value) {
|
||||
return '${value.year.toString().padLeft(4, '0')}-'
|
||||
'${value.month.toString().padLeft(2, '0')}-'
|
||||
'${value.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
int _int(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
|
||||
@@ -140,10 +140,8 @@ class GelbooruProvider implements ContentProvider, CommentProvider {
|
||||
List<String> _topTags(TopPeriodFilter period) {
|
||||
return switch (period) {
|
||||
TopPeriodFilter.none => const [],
|
||||
TopPeriodFilter.month ||
|
||||
TopPeriodFilter.year ||
|
||||
TopPeriodFilter.allTime =>
|
||||
const ['sort:score:desc'],
|
||||
TopPeriodFilter.month || TopPeriodFilter.year => const [],
|
||||
TopPeriodFilter.allTime => const ['sort:score:desc'],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -166,10 +166,8 @@ class MoebooruProvider
|
||||
List<String> _topTags(TopPeriodFilter period) {
|
||||
return switch (period) {
|
||||
TopPeriodFilter.none => const [],
|
||||
TopPeriodFilter.month ||
|
||||
TopPeriodFilter.year ||
|
||||
TopPeriodFilter.allTime =>
|
||||
const ['order:score'],
|
||||
TopPeriodFilter.month || TopPeriodFilter.year => const [],
|
||||
TopPeriodFilter.allTime => const ['order:score'],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,8 @@ class Rule34Provider extends GelbooruProvider {
|
||||
}) async {
|
||||
final topTags = switch (topPeriod) {
|
||||
TopPeriodFilter.none => const <String>[],
|
||||
TopPeriodFilter.month ||
|
||||
TopPeriodFilter.year ||
|
||||
TopPeriodFilter.allTime =>
|
||||
const ['sort:score:desc'],
|
||||
TopPeriodFilter.month || TopPeriodFilter.year => const <String>[],
|
||||
TopPeriodFilter.allTime => const ['sort:score:desc'],
|
||||
};
|
||||
final response = await dio.get<dynamic>(
|
||||
'/index.php',
|
||||
|
||||
@@ -70,6 +70,23 @@ class CollectionRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Result<void>> addPosts(String collectionId, List<Post> posts) async {
|
||||
if (posts.isEmpty) return const Success(null);
|
||||
await _postRepository.cachePosts(posts);
|
||||
return _databaseService.safeWrite((isar) async {
|
||||
final now = DateTime.now();
|
||||
await isar.collectionPostEntitys.putAll([
|
||||
for (final post in posts)
|
||||
CollectionPostEntity()
|
||||
..linkKey = '$collectionId:${post.providerId}:${post.id}'
|
||||
..collectionId = collectionId
|
||||
..postId = post.id
|
||||
..providerId = post.providerId
|
||||
..addedAt = now,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Result<void>> removePost(
|
||||
String collectionId,
|
||||
String postId,
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import '../models/post.dart';
|
||||
|
||||
enum MediaQualityMode {
|
||||
auto,
|
||||
dataSaver,
|
||||
highQuality;
|
||||
|
||||
static MediaQualityMode fromName(String value) {
|
||||
return MediaQualityMode.values.firstWhere(
|
||||
(mode) => mode.name == value,
|
||||
orElse: () => MediaQualityMode.auto,
|
||||
);
|
||||
}
|
||||
|
||||
String get label {
|
||||
return switch (this) {
|
||||
MediaQualityMode.auto => 'Auto',
|
||||
MediaQualityMode.dataSaver => 'Data saver',
|
||||
MediaQualityMode.highQuality => 'High quality',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MediaUrlSelector {
|
||||
const MediaUrlSelector._();
|
||||
|
||||
static List<String> feed(Post post) {
|
||||
return _compact([post.previewUrl, post.sampleUrl]);
|
||||
}
|
||||
|
||||
static List<String> preview(Post post) {
|
||||
return _compact([post.sampleUrl, post.previewUrl, post.fileUrl]);
|
||||
}
|
||||
|
||||
static List<String> details(
|
||||
Post post, {
|
||||
MediaQualityMode mode = MediaQualityMode.auto,
|
||||
}) {
|
||||
if (_isVideo(post)) {
|
||||
return _compact([post.fileUrl, post.sampleUrl, post.previewUrl]);
|
||||
}
|
||||
if (_isGif(post)) {
|
||||
return switch (mode) {
|
||||
MediaQualityMode.dataSaver =>
|
||||
_compact([post.sampleUrl, post.previewUrl, post.fileUrl]),
|
||||
_ => _compact([post.fileUrl, post.sampleUrl, post.previewUrl]),
|
||||
};
|
||||
}
|
||||
return switch (mode) {
|
||||
MediaQualityMode.dataSaver =>
|
||||
_compact([post.sampleUrl, post.previewUrl, post.fileUrl]),
|
||||
MediaQualityMode.highQuality =>
|
||||
_compact([post.fileUrl, post.sampleUrl, post.previewUrl]),
|
||||
MediaQualityMode.auto =>
|
||||
_compact([post.sampleUrl, post.fileUrl, post.previewUrl]),
|
||||
};
|
||||
}
|
||||
|
||||
static String? download(Post post) {
|
||||
return _compact([post.fileUrl, post.sampleUrl, post.previewUrl])
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
static List<String> video(Post post) {
|
||||
return _compact([
|
||||
post.fileUrl,
|
||||
if (_looksLikeVideoUrl(post.sampleUrl)) post.sampleUrl,
|
||||
if (_looksLikeVideoUrl(post.previewUrl)) post.previewUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
static bool isVideo(Post post) => _isVideo(post);
|
||||
static bool isGif(Post post) => _isGif(post);
|
||||
|
||||
static List<String> _compact(Iterable<String> urls) {
|
||||
final seen = <String>{};
|
||||
return urls
|
||||
.map((url) => url.trim())
|
||||
.where((url) => url.isNotEmpty && seen.add(url))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
static bool _isVideo(Post post) {
|
||||
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
|
||||
return value.contains('video') ||
|
||||
value.contains('.webm') ||
|
||||
value.contains('.mp4') ||
|
||||
value.contains('.mov');
|
||||
}
|
||||
|
||||
static bool _isGif(Post post) {
|
||||
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
|
||||
return value.contains('gif') || value.contains('.gif');
|
||||
}
|
||||
|
||||
static bool _looksLikeVideoUrl(String url) {
|
||||
final value = url.toLowerCase();
|
||||
return value.contains('.webm') ||
|
||||
value.contains('.mp4') ||
|
||||
value.contains('.mov');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user