// lib/services/cache_service.dart import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart' as prefs; import 'package:idb_shim/idb_browser.dart' if (dart.library.io) 'package:idb_shim/idb_io.dart'; import '../models/video_result.dart'; /// Storage provider to handle platform differences abstract class StorageProvider { Future initialize(); Future write(String key, Uint8List data); Future read(String key); Future delete(String key); Future clear(); Future getTotalSize(); } /// IndexedDB implementation for web platform class WebStorageProvider implements StorageProvider { late Database _db; static const String _storeName = 'binary_store'; @override Future initialize() async { final factory = getIdbFactory()!; _db = await factory.open( 'aitube_cache', version: 1, onUpgradeNeeded: (VersionChangeEvent event) { final db = event.database; db.createObjectStore(_storeName); }, ); } @override Future write(String key, Uint8List data) async { final txn = _db.transaction(_storeName, 'readwrite'); final store = txn.objectStore(_storeName); await store.put(data, key); } @override Future read(String key) async { final txn = _db.transaction(_storeName, 'readonly'); final store = txn.objectStore(_storeName); final data = await store.getObject(key); if (data == null) return null; return data as Uint8List; } @override Future delete(String key) async { final txn = _db.transaction(_storeName, 'readwrite'); final store = txn.objectStore(_storeName); await store.delete(key); } @override Future clear() async { final txn = _db.transaction(_storeName, 'readwrite'); final store = txn.objectStore(_storeName); await store.clear(); } @override Future getTotalSize() async { final txn = _db.transaction(_storeName, 'readonly'); final store = txn.objectStore(_storeName); final keys = await store.getAllKeys(); int totalSize = 0; for (final key in keys) { final data = await store.getObject(key); if (data is Uint8List) { totalSize += data.length; } } return totalSize; } } /// Memory-based implementation for web platform class MemoryStorageProvider implements StorageProvider { final Map _storage = {}; @override Future initialize() async {} @override Future write(String key, Uint8List data) async { _storage[key] = data; } @override Future read(String key) async { return _storage[key]; } @override Future delete(String key) async { _storage.remove(key); } @override Future clear() async { _storage.clear(); } @override Future getTotalSize() async { return _storage.values.fold(0, (total, data) => total + data.length); } } class CacheService { static final CacheService _instance = CacheService._internal(); factory CacheService() => _instance; late final prefs.SharedPreferences _prefs; late final StorageProvider _storage; final _statsController = StreamController.broadcast(); static const String _metadataPrefix = 'metadata_'; static const String _videoPrefix = 'video_'; static const String _thumbnailPrefix = 'thumb_'; static const Duration _cacheExpiry = Duration(days: 7); Stream get statsStream => _statsController.stream; CacheService._internal() { // Use IndexedDB for web, memory storage for testing/development _storage = kIsWeb ? WebStorageProvider() : MemoryStorageProvider(); } Future initialize() async { await _storage.initialize(); _prefs = await prefs.SharedPreferences.getInstance(); await _cleanExpiredEntries(); await _updateStats(); } Future _cleanExpiredEntries() async { final now = DateTime.now(); final keys = _prefs.getKeys().where((k) => k.startsWith(_metadataPrefix)); for (final key in keys) { final metadata = _prefs.getString(key); if (metadata != null) { final data = json.decode(metadata); final timestamp = DateTime.parse(data['timestamp']); if (now.difference(timestamp) > _cacheExpiry) { await _removeEntry(key.substring(_metadataPrefix.length)); } } } } Future _removeEntry(String key) async { await _prefs.remove('$_metadataPrefix$key'); await _storage.delete(key); await _updateStats(); } Future _updateStats() async { final totalSize = await _storage.getTotalSize(); final totalItems = _prefs.getKeys() .where((k) => k.startsWith(_metadataPrefix)) .length; _statsController.add(CacheStats( totalItems: totalItems, totalSizeMB: totalSize / (1024 * 1024), )); } Future cacheSearchResults(String query, List results) async { final key = 'search_$query'; final data = Uint8List.fromList(utf8.encode(json.encode({ 'query': query, 'results': results.map((r) => r.toJson()).toList(), }))); await _storage.write(key, data); await _prefs.setString('$_metadataPrefix$key', json.encode({ 'timestamp': DateTime.now().toIso8601String(), 'type': 'search', })); await _updateStats(); } Future?> getSearchResults(String query) async { final key = 'search_$query'; final data = await _storage.read(key); if (data == null) return null; final decoded = json.decode(utf8.decode(data)); return (decoded['results'] as List) .map((r) => VideoResult.fromJson(r as Map)) .toList(); } Future cacheVideoData(String videoId, String videoData) async { final key = '$_videoPrefix$videoId'; final data = _extractVideoData(videoData); await _storage.write(key, data); await _prefs.setString('$_metadataPrefix$key', json.encode({ 'timestamp': DateTime.now().toIso8601String(), 'type': 'video', })); await _updateStats(); } Future getVideoData(String videoId) async { final key = '$_videoPrefix$videoId'; final data = await _storage.read(key); if (data == null) return null; return 'data:video/mp4;base64,${base64Encode(data)}'; } Future cacheThumbnail(String videoId, String thumbnailData) async { final key = '$_thumbnailPrefix$videoId'; final data = _extractImageData(thumbnailData); await _storage.write(key, data); await _prefs.setString('$_metadataPrefix$key', json.encode({ 'timestamp': DateTime.now().toIso8601String(), 'type': 'thumbnail', })); await _updateStats(); } Future getThumbnail(String videoId) async { final key = '$_thumbnailPrefix$videoId'; final data = await _storage.read(key); if (data == null) return null; return 'data:image/jpeg;base64,${base64Encode(data)}'; } Uint8List _extractVideoData(String videoData) { final parts = videoData.split(','); if (parts.length != 2) throw Exception('Invalid video data format'); return base64Decode(parts[1]); } Uint8List _extractImageData(String imageData) { final parts = imageData.split(','); if (parts.length != 2) throw Exception('Invalid image data format'); return base64Decode(parts[1]); } Future delete(String key) async { await _storage.delete('$_videoPrefix$key'); await _prefs.remove('$_metadataPrefix$_videoPrefix$key'); await _updateStats(); } Future clearCache() async { await _storage.clear(); final keys = _prefs.getKeys().where((k) => k.startsWith(_metadataPrefix)); for (final key in keys) { await _prefs.remove(key); } await _updateStats(); } Future cacheSearchResult(String query, VideoResult result, int searchCount) async { final key = 'search_${query}_$searchCount'; final data = Uint8List.fromList(utf8.encode(json.encode({ 'query': query, 'searchCount': searchCount, 'result': result.toJson(), }))); await _storage.write(key, data); await _prefs.setString('$_metadataPrefix$key', json.encode({ 'timestamp': DateTime.now().toIso8601String(), 'type': 'search', })); await _updateStats(); } Future> getCachedSearchResults(String query) async { final results = []; final searchKeys = _prefs.getKeys() .where((k) => k.startsWith('${_metadataPrefix}search_$query')); for (final key in searchKeys) { final data = await _storage.read(key.substring(_metadataPrefix.length)); if (data != null) { final decoded = json.decode(utf8.decode(data)); results.add(VideoResult.fromJson(decoded['result'] as Map)); } } return results..sort((a, b) => a.createdAt.compareTo(b.createdAt)); } Future getLastSearchCount(String query) async { final searchKeys = _prefs.getKeys() .where((k) => k.startsWith('${_metadataPrefix}search_$query')) .toList(); if (searchKeys.isEmpty) return 0; int maxCount = -1; for (final key in searchKeys) { final match = RegExp(r'search_.*_(\d+)$').firstMatch(key); if (match != null) { final count = int.parse(match.group(1)!); if (count > maxCount) maxCount = count; } } return maxCount + 1; } void dispose() { _statsController.close(); } } class CacheStats { final int totalItems; final double totalSizeMB; CacheStats({ required this.totalItems, required this.totalSizeMB, }); }