mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-22 22:48:04 +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,308 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// Represents a download task
|
||||
class DownloadTask {
|
||||
final String id;
|
||||
final String title;
|
||||
final String url;
|
||||
final String filePath;
|
||||
final DownloadStatus status;
|
||||
final double progress;
|
||||
final int? totalSize;
|
||||
final int downloadedSize;
|
||||
|
||||
DownloadTask({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.filePath,
|
||||
this.status = DownloadStatus.pending,
|
||||
this.progress = 0.0,
|
||||
this.totalSize,
|
||||
this.downloadedSize = 0,
|
||||
});
|
||||
|
||||
DownloadTask copyWith({
|
||||
DownloadStatus? status,
|
||||
double? progress,
|
||||
int? downloadedSize,
|
||||
}) =>
|
||||
DownloadTask(
|
||||
id: id,
|
||||
title: title,
|
||||
url: url,
|
||||
filePath: filePath,
|
||||
status: status ?? this.status,
|
||||
progress: progress ?? this.progress,
|
||||
totalSize: totalSize,
|
||||
downloadedSize: downloadedSize ?? this.downloadedSize,
|
||||
);
|
||||
}
|
||||
|
||||
enum DownloadStatus {
|
||||
pending,
|
||||
downloading,
|
||||
paused,
|
||||
completed,
|
||||
failed,
|
||||
cancelled,
|
||||
}
|
||||
|
||||
/// Service for managing downloads
|
||||
class DownloadService {
|
||||
static const _downloadDir = './downloads';
|
||||
static const _maxConcurrentDownloads = 3;
|
||||
static const _maxStorageGb = 50;
|
||||
|
||||
final List<DownloadTask> _downloads = [];
|
||||
final List<String> _activeDownloads = [];
|
||||
|
||||
/// Initialize download directory
|
||||
static Future<void> initialize() async {
|
||||
final dir = Directory(_downloadDir);
|
||||
if (!dir.existsSync()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new download
|
||||
Future<DownloadTask> startDownload({
|
||||
required String id,
|
||||
required String title,
|
||||
required String url,
|
||||
}) async {
|
||||
final filePath = '$_downloadDir/$id.mp4';
|
||||
final task = DownloadTask(
|
||||
id: id,
|
||||
title: title,
|
||||
url: url,
|
||||
filePath: filePath,
|
||||
);
|
||||
|
||||
_downloads.add(task);
|
||||
_processDownloadQueue();
|
||||
return task;
|
||||
}
|
||||
|
||||
/// Pause download
|
||||
void pauseDownload(String id) {
|
||||
final index = _downloads.indexWhere((t) => t.id == id);
|
||||
if (index != -1) {
|
||||
_downloads[index] = _downloads[index].copyWith(
|
||||
status: DownloadStatus.paused,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resume download
|
||||
void resumeDownload(String id) {
|
||||
final index = _downloads.indexWhere((t) => t.id == id);
|
||||
if (index != -1) {
|
||||
_downloads[index] = _downloads[index].copyWith(
|
||||
status: DownloadStatus.pending,
|
||||
);
|
||||
_processDownloadQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel download
|
||||
void cancelDownload(String id) {
|
||||
final index = _downloads.indexWhere((t) => t.id == id);
|
||||
if (index != -1) {
|
||||
_downloads.removeAt(index);
|
||||
_activeDownloads.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all downloads
|
||||
List<DownloadTask> getAllDownloads() => List.from(_downloads);
|
||||
|
||||
/// Get download progress
|
||||
double getDownloadProgress(String id) {
|
||||
final task = _downloads.firstWhere((t) => t.id == id, orElse: () => DownloadTask(
|
||||
id: id,
|
||||
title: '',
|
||||
url: '',
|
||||
filePath: '',
|
||||
));
|
||||
return task.progress;
|
||||
}
|
||||
|
||||
/// Get available storage
|
||||
Future<int> getAvailableStorage() async {
|
||||
final dir = Directory(_downloadDir);
|
||||
if (!dir.existsSync()) return _maxStorageGb * 1024 * 1024 * 1024;
|
||||
|
||||
int totalSize = 0;
|
||||
final files = dir.listSync(recursive: true);
|
||||
for (final file in files) {
|
||||
if (file is File) {
|
||||
totalSize += file.lengthSync();
|
||||
}
|
||||
}
|
||||
|
||||
return (_maxStorageGb * 1024 * 1024 * 1024) - totalSize;
|
||||
}
|
||||
|
||||
/// Clean up old downloads based on space
|
||||
Future<void> cleanupOldDownloads() async {
|
||||
final available = await getAvailableStorage();
|
||||
if (available < 1024 * 1024 * 1024) {
|
||||
// Less than 1GB free
|
||||
final dir = Directory(_downloadDir);
|
||||
final files = dir.listSync()
|
||||
.whereType<File>()
|
||||
.toList();
|
||||
|
||||
// Sort by modification time, delete oldest first
|
||||
files.sort((a, b) =>
|
||||
a.statSync().modified.compareTo(b.statSync().modified));
|
||||
|
||||
for (final file in files.take(files.length ~/ 3)) {
|
||||
try {
|
||||
file.deleteSync();
|
||||
} catch (e) {
|
||||
print('Error deleting file: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process download queue
|
||||
Future<void> _processDownloadQueue() async {
|
||||
while (_activeDownloads.length < _maxConcurrentDownloads) {
|
||||
final pendingIndex =
|
||||
_downloads.indexWhere((t) => t.status == DownloadStatus.pending);
|
||||
if (pendingIndex == -1) break;
|
||||
|
||||
final task = _downloads[pendingIndex];
|
||||
_activeDownloads.add(task.id);
|
||||
|
||||
_downloads[pendingIndex] =
|
||||
task.copyWith(status: DownloadStatus.downloading);
|
||||
|
||||
try {
|
||||
await _downloadFile(task);
|
||||
} catch (e) {
|
||||
print('Download error: $e');
|
||||
final index = _downloads.indexWhere((t) => t.id == task.id);
|
||||
if (index != -1) {
|
||||
_downloads[index] =
|
||||
_downloads[index].copyWith(status: DownloadStatus.failed);
|
||||
}
|
||||
} finally {
|
||||
_activeDownloads.remove(task.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Download file with progress tracking
|
||||
Future<void> _downloadFile(DownloadTask task) async {
|
||||
// Simplified version - in production use dio or http with progress tracking
|
||||
final file = File(task.filePath);
|
||||
if (!file.existsSync()) {
|
||||
file.createSync(recursive: true);
|
||||
}
|
||||
|
||||
// Stream download and update progress
|
||||
// This is a placeholder - implement with actual download logic
|
||||
for (int i = 0; i <= 100; i++) {
|
||||
final index = _downloads.indexWhere((t) => t.id == task.id);
|
||||
if (index != -1) {
|
||||
_downloads[index] = _downloads[index].copyWith(
|
||||
progress: i.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if paused or cancelled
|
||||
final currentTask = _downloads.firstWhere((t) => t.id == task.id, orElse: () => task);
|
||||
if (currentTask.status == DownloadStatus.paused ||
|
||||
!_activeDownloads.contains(task.id)) {
|
||||
break;
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
final index = _downloads.indexWhere((t) => t.id == task.id);
|
||||
if (index != -1) {
|
||||
_downloads[index] = _downloads[index].copyWith(
|
||||
status: DownloadStatus.completed,
|
||||
progress: 100.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if file is available for offline playback
|
||||
bool isAvailableOffline(String id) {
|
||||
final task = _downloads.firstWhere(
|
||||
(t) => t.id == id,
|
||||
orElse: () => DownloadTask(
|
||||
id: id,
|
||||
title: '',
|
||||
url: '',
|
||||
filePath: '',
|
||||
),
|
||||
);
|
||||
if (task.status == DownloadStatus.completed) {
|
||||
return File(task.filePath).existsSync();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get file path for offline content
|
||||
String? getOfflineFilePath(String id) {
|
||||
final task = _downloads.firstWhere(
|
||||
(t) => t.id == id && t.status == DownloadStatus.completed,
|
||||
orElse: () => DownloadTask(
|
||||
id: id,
|
||||
title: '',
|
||||
url: '',
|
||||
filePath: '',
|
||||
),
|
||||
);
|
||||
|
||||
if (File(task.filePath).existsSync()) {
|
||||
return task.filePath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Riverpod providers
|
||||
|
||||
final downloadServiceProvider = Provider<DownloadService>((ref) {
|
||||
return DownloadService();
|
||||
});
|
||||
|
||||
final downloadsProvider =
|
||||
StateNotifierProvider<DownloadsNotifier, List<DownloadTask>>((ref) {
|
||||
return DownloadsNotifier();
|
||||
});
|
||||
|
||||
class DownloadsNotifier extends StateNotifier<List<DownloadTask>> {
|
||||
DownloadsNotifier() : super([]);
|
||||
|
||||
void addDownload(DownloadTask task) {
|
||||
state = [...state, task];
|
||||
}
|
||||
|
||||
void updateDownload(DownloadTask task) {
|
||||
state = [
|
||||
for (final d in state)
|
||||
if (d.id == task.id) task else d,
|
||||
];
|
||||
}
|
||||
|
||||
void removeDownload(String id) {
|
||||
state = state.where((d) => d.id != id).toList();
|
||||
}
|
||||
|
||||
void clearCompleted() {
|
||||
state = state
|
||||
.where((d) => d.status != DownloadStatus.completed)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Subtitle track information
|
||||
class SubtitleTrack {
|
||||
final String id;
|
||||
final String name;
|
||||
final String language;
|
||||
final String url;
|
||||
final bool isEnabled;
|
||||
|
||||
SubtitleTrack({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.language,
|
||||
required this.url,
|
||||
this.isEnabled = false,
|
||||
});
|
||||
|
||||
SubtitleTrack copyWith({bool? isEnabled}) => SubtitleTrack(
|
||||
id: id,
|
||||
name: name,
|
||||
language: language,
|
||||
url: url,
|
||||
isEnabled: isEnabled ?? this.isEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
/// Subtitle content with timing
|
||||
class SubtitleEntry {
|
||||
final int index;
|
||||
final Duration startTime;
|
||||
final Duration endTime;
|
||||
final String text;
|
||||
|
||||
SubtitleEntry({
|
||||
required this.index,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.text,
|
||||
});
|
||||
}
|
||||
|
||||
/// Service for handling subtitles
|
||||
class SubtitleService {
|
||||
static const _srtPattern = r'(\d+)\n(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})\n([\s\S]*?)(?=\n\n|\Z)';
|
||||
|
||||
/// Parse SRT subtitle content
|
||||
static List<SubtitleEntry> parseSrt(String content) {
|
||||
final entries = <SubtitleEntry>[];
|
||||
final blocks = content.split('\n\n');
|
||||
|
||||
for (final block in blocks) {
|
||||
if (block.trim().isEmpty) continue;
|
||||
|
||||
final lines = block.trim().split('\n');
|
||||
if (lines.length < 3) continue;
|
||||
|
||||
try {
|
||||
final index = int.parse(lines[0]);
|
||||
final timeLine = lines[1];
|
||||
final text = lines.sublist(2).join('\n').trim();
|
||||
|
||||
// Parse timing: 00:00:01,000 --> 00:00:05,000
|
||||
final times = timeLine.split('-->');
|
||||
if (times.length != 2) continue;
|
||||
|
||||
final startTime = _parseTime(times[0].trim());
|
||||
final endTime = _parseTime(times[1].trim());
|
||||
|
||||
entries.add(SubtitleEntry(
|
||||
index: index,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
text: text,
|
||||
));
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// Parse WebVTT subtitle content
|
||||
static List<SubtitleEntry> parseWebVtt(String content) {
|
||||
final entries = <SubtitleEntry>[];
|
||||
final blocks = content.split('\n\n');
|
||||
|
||||
for (final block in blocks) {
|
||||
if (block.trim().isEmpty || block.startsWith('WEBVTT')) continue;
|
||||
|
||||
final lines = block.trim().split('\n');
|
||||
if (lines.length < 2) continue;
|
||||
|
||||
try {
|
||||
final timeLine = lines[0];
|
||||
final text = lines.sublist(1).join('\n').trim();
|
||||
|
||||
// Parse timing: 00:00:01.000 --> 00:00:05.000
|
||||
final times = timeLine.split('-->');
|
||||
if (times.length != 2) continue;
|
||||
|
||||
final startTime = _parseWebVttTime(times[0].trim());
|
||||
final endTime = _parseWebVttTime(times[1].trim());
|
||||
|
||||
entries.add(SubtitleEntry(
|
||||
index: entries.length,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
text: text,
|
||||
));
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// Get subtitle at specific time
|
||||
static String? getSubtitleAtTime(List<SubtitleEntry> entries, Duration time) {
|
||||
for (final entry in entries) {
|
||||
if (time.compareTo(entry.startTime) >= 0 && time.compareTo(entry.endTime) <= 0) {
|
||||
return entry.text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Download subtitle from URL
|
||||
static Future<String> downloadSubtitle(String url) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
}
|
||||
throw Exception('Failed to download subtitle: ${response.statusCode}');
|
||||
} catch (e) {
|
||||
throw Exception('Error downloading subtitle: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse time format HH:MM:SS,mmm (SRT)
|
||||
static Duration _parseTime(String time) {
|
||||
final parts = time.split(':');
|
||||
final hours = int.parse(parts[0]);
|
||||
final minutes = int.parse(parts[1]);
|
||||
final secondsParts = parts[2].split(',');
|
||||
final seconds = int.parse(secondsParts[0]);
|
||||
final milliseconds = int.parse(secondsParts[1]);
|
||||
|
||||
return Duration(
|
||||
hours: hours,
|
||||
minutes: minutes,
|
||||
seconds: seconds,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse time format HH:MM:SS.mmm (WebVTT)
|
||||
static Duration _parseWebVttTime(String time) {
|
||||
final parts = time.split(':');
|
||||
final hours = int.parse(parts[0]);
|
||||
final minutes = int.parse(parts[1]);
|
||||
final secondsParts = parts[2].split('.');
|
||||
final seconds = int.parse(secondsParts[0]);
|
||||
final milliseconds = int.parse(secondsParts[1].padEnd(3, '0'));
|
||||
|
||||
return Duration(
|
||||
hours: hours,
|
||||
minutes: minutes,
|
||||
seconds: seconds,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user