mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 15:08:07 +02:00
Add download and subtitle services, enhance UI with continue watching and quality selector widgets
- Implemented DownloadService for managing download tasks, including start, pause, resume, and cancel functionalities. - Created SubtitleService for handling subtitle tracks, parsing SRT and WebVTT formats, and downloading subtitles. - Added ContinueWatchingWidget to display user's watch history with progress indicators. - Developed QualitySelectorWidget for selecting video quality during playback, including auto quality adjustment. - Introduced TrendingWidget and RecentlyAddedWidget to showcase trending and recently added content.
This commit is contained in:
1 parent
a9a1127d6b
commit
91eb1ce5a0
20 files changed
+6574
-1
No files matched your search
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// Optimized cache service for memory management
|
||||
class CacheService {
|
||||
static const _maxMemoryCacheSizeMb = 200;
|
||||
static const _maxImageCacheItems = 500;
|
||||
|
||||
final Map<String, CacheEntry> _cache = {};
|
||||
int _totalSizeBytes = 0;
|
||||
|
||||
/// Cache entry with metadata
|
||||
class CacheEntry<T> {
|
||||
final T data;
|
||||
final DateTime timestamp;
|
||||
final int sizeEstimate;
|
||||
int accessCount;
|
||||
DateTime lastAccess;
|
||||
|
||||
CacheEntry({
|
||||
required this.data,
|
||||
required this.sizeEstimate,
|
||||
}) : timestamp = DateTime.now(),
|
||||
lastAccess = DateTime.now(),
|
||||
accessCount = 0;
|
||||
|
||||
bool isExpired({Duration ttl = const Duration(hours: 24)}) {
|
||||
return DateTime.now().difference(timestamp) > ttl;
|
||||
}
|
||||
|
||||
void markAccess() {
|
||||
accessCount++;
|
||||
lastAccess = DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache data with automatic size management
|
||||
void cache<T>(String key, T value, {int estimatedSize = 1000}) {
|
||||
// Remove old entry if exists
|
||||
if (_cache.containsKey(key)) {
|
||||
_totalSizeBytes -= _cache[key]!.sizeEstimate;
|
||||
}
|
||||
|
||||
_cache[key] = CacheEntry(
|
||||
data: value,
|
||||
sizeEstimate: estimatedSize,
|
||||
);
|
||||
_totalSizeBytes += estimatedSize;
|
||||
|
||||
// Trim cache if over size limit
|
||||
_trimCacheIfNeeded();
|
||||
}
|
||||
|
||||
/// Get cached data
|
||||
T? get<T>(String key) {
|
||||
final entry = _cache[key] as CacheEntry?;
|
||||
if (entry != null && !entry.isExpired()) {
|
||||
entry.markAccess();
|
||||
return entry.data as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if key exists and is valid
|
||||
bool has(String key) {
|
||||
final entry = _cache[key];
|
||||
return entry != null && !entry.isExpired();
|
||||
}
|
||||
|
||||
/// Get cache stats
|
||||
Map<String, dynamic> getStats() => {
|
||||
'totalSize': _totalSizeBytes,
|
||||
'totalSizeMb': _totalSizeBytes / (1024 * 1024),
|
||||
'itemCount': _cache.length,
|
||||
'averageSizePerItem': _cache.isEmpty ? 0 : _totalSizeBytes ~/ _cache.length,
|
||||
};
|
||||
|
||||
/// Clear all cache
|
||||
void clear() {
|
||||
_cache.clear();
|
||||
_totalSizeBytes = 0;
|
||||
}
|
||||
|
||||
/// Clear expired entries
|
||||
void clearExpired() {
|
||||
final now = DateTime.now();
|
||||
final toRemove = _cache.entries
|
||||
.where((e) => e.value.isExpired())
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
|
||||
for (final key in toRemove) {
|
||||
_totalSizeBytes -= _cache[key]!.sizeEstimate;
|
||||
_cache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim cache by removing least recently used entries
|
||||
void _trimCacheIfNeeded() {
|
||||
final maxSize = _maxMemoryCacheSizeMb * 1024 * 1024;
|
||||
if (_totalSizeBytes > maxSize) {
|
||||
// Remove least recently accessed entries
|
||||
final sorted = _cache.entries.toList()
|
||||
..sort((a, b) => a.value.accessCount.compareTo(b.value.accessCount));
|
||||
|
||||
for (final entry in sorted) {
|
||||
if (_totalSizeBytes <= maxSize * 0.8) break;
|
||||
_totalSizeBytes -= entry.value.sizeEstimate;
|
||||
_cache.remove(entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Image cache with size limiting
|
||||
class ImageCacheManager {
|
||||
static const _maxImageMemoryMb = 100;
|
||||
static const _maxImages = 500;
|
||||
|
||||
final Map<String, CachedImage> _imageCache = {};
|
||||
int _totalMemoryBytes = 0;
|
||||
|
||||
class CachedImage {
|
||||
final String url;
|
||||
final int sizeBytes;
|
||||
final DateTime cachedAt;
|
||||
int accessCount = 0;
|
||||
|
||||
CachedImage({
|
||||
required this.url,
|
||||
required this.sizeBytes,
|
||||
}) : cachedAt = DateTime.now();
|
||||
}
|
||||
|
||||
/// Add image to cache
|
||||
void cacheImage(String url, int sizeEstimateBytes) {
|
||||
if (_imageCache.length >= _maxImages) {
|
||||
_evictLeastUsed();
|
||||
}
|
||||
|
||||
_imageCache[url] = CachedImage(
|
||||
url: url,
|
||||
sizeBytes: sizeEstimateBytes,
|
||||
);
|
||||
_totalMemoryBytes += sizeEstimateBytes;
|
||||
|
||||
// Trim if over size
|
||||
while (_totalMemoryBytes > _maxImageMemoryMb * 1024 * 1024) {
|
||||
_evictLeastUsed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if image is cached
|
||||
bool isCached(String url) => _imageCache.containsKey(url);
|
||||
|
||||
/// Mark image as accessed
|
||||
void markAccessed(String url) {
|
||||
if (_imageCache.containsKey(url)) {
|
||||
_imageCache[url]!.accessCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Evict least used image
|
||||
void _evictLeastUsed() {
|
||||
if (_imageCache.isEmpty) return;
|
||||
|
||||
final leastUsed = _imageCache.entries
|
||||
.reduce((a, b) => a.value.accessCount < b.value.accessCount ? a : b);
|
||||
|
||||
_totalMemoryBytes -= leastUsed.value.sizeBytes;
|
||||
_imageCache.remove(leastUsed.key);
|
||||
}
|
||||
|
||||
/// Get cache stats
|
||||
Map<String, dynamic> getStats() => {
|
||||
'totalMemoryMb': _totalMemoryBytes / (1024 * 1024),
|
||||
'cachedImages': _imageCache.length,
|
||||
'maxImages': _maxImages,
|
||||
};
|
||||
|
||||
/// Clear all image cache
|
||||
void clear() {
|
||||
_imageCache.clear();
|
||||
_totalMemoryBytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Riverpod providers
|
||||
|
||||
final cacheServiceProvider = Provider<CacheService>((ref) {
|
||||
return CacheService();
|
||||
});
|
||||
|
||||
final imageCacheManagerProvider = Provider<ImageCacheManager>((ref) {
|
||||
return ImageCacheManager();
|
||||
});
|
||||
|
||||
/// Provider for cache statistics
|
||||
final cacheStatsProvider = StateNotifierProvider<
|
||||
CacheStatsNotifier,
|
||||
Map<String, dynamic>>((ref) {
|
||||
final cacheService = ref.watch(cacheServiceProvider);
|
||||
return CacheStatsNotifier(cacheService);
|
||||
});
|
||||
|
||||
class CacheStatsNotifier extends StateNotifier<Map<String, dynamic>> {
|
||||
final CacheService _cacheService;
|
||||
|
||||
CacheStatsNotifier(this._cacheService)
|
||||
: super(_cacheService.getStats());
|
||||
|
||||
void refreshStats() {
|
||||
state = _cacheService.getStats();
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
_cacheService.clear();
|
||||
state = _cacheService.getStats();
|
||||
}
|
||||
|
||||
void clearExpired() {
|
||||
_cacheService.clearExpired();
|
||||
state = _cacheService.getStats();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user