import 'dart:async'; import 'dart:convert'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:madari_client/features/connection/types/stremio.dart'; import 'package:madari_client/features/doc_viewer/container/doc_viewer.dart'; import 'package:madari_client/features/doc_viewer/types/doc_source.dart'; import 'package:pocketbase/pocketbase.dart'; import '../../connection/services/stremio_service.dart'; import '../../downloads/service/service.dart'; class StremioStreamSelector extends StatefulWidget { final StremioService stremio; final Meta item; final String? episode; final String? season; final String id; final Widget? library; const StremioStreamSelector({ super.key, required this.id, required this.stremio, required this.item, this.episode, this.season, this.library, }); @override State createState() => _StremioStreamSelectorState(); } class _StremioStreamSelectorState extends State { late final Stream> _stream; final Map _downloadStatus = {}; final Map _downloadProgress = {}; DownloadService? _downloadService; StreamSubscription? _downloadSubscription; @override void initState() { super.initState(); if (!kIsWeb) _downloadService = DownloadService.instance; _stream = widget.stremio .getStreams( widget.item.type, widget.id, episode: widget.episode, season: widget.season, ) .map((item) { return [ if (widget.item.type == "movie") for (final item in (widget.stremio.configParsed.movieIframe ?? [])) VideoStream( title: widget.item.name, behaviorHints: { "filename": widget.item.name, "iframe": item, }, ), if (widget.item.type == "series") for (final item in (widget.stremio.configParsed.seriesIframe ?? [])) VideoStream( title: widget.item.name, behaviorHints: { "filename": widget.item.name, "iframe": item, }, ), ...item, ]; }); _setupDownloadListener(); _checkExistingDownloads(); } @override void dispose() { _downloadSubscription?.cancel(); super.dispose(); } void _setupDownloadListener() { _downloadSubscription = _downloadService?.updates.listen((update) { if (!mounted) return; switch (update) { case TaskStatusUpdate(): final index = int.tryParse(update.task.taskId.split('_').last); if (index != null) { setState(() { _downloadStatus[index] = update.status; }); } case TaskProgressUpdate(): final index = int.tryParse(update.task.taskId.split('_').last); if (index != null) { setState(() { _downloadProgress[index] = update.progress; }); } } }); } Future _checkExistingDownloads() async { final downloads = await _downloadService?.getAllDownloads(); if (!mounted) return; setState(() { for (var record in (downloads ?? [])) { final index = int.tryParse(record.task.taskId.split('_').last); if (index != null) { _downloadStatus[index] = record.status; _downloadProgress[index] = record.progress; } } }); } String _getFileName(VideoStream item) { return item.behaviorHints?["filename"] ?? "${widget.item.name}.mp4"; } Future _startDownload(VideoStream item, int index) async { final fileName = _getFileName(item); final url = item.url; if (url == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No URL available for download')), ); } return; } final task = DownloadTask( taskId: 'download_$index', url: url, displayName: fileName.split("/").last, filename: fileName.split("/").last, baseDirectory: BaseDirectory.applicationDocuments, updates: Updates.statusAndProgress, ); try { await _downloadService?.startDownload(task); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Download failed: $e')), ); } } } Widget _buildDownloadButton(VideoStream item, int index) { if (isWeb) { return const SizedBox( width: 1, height: 1, ); } final status = _downloadStatus[index]; final progress = _downloadProgress[index] ?? 0.0; switch (status) { case TaskStatus.complete: return IconButton( icon: const Icon(Icons.play_circle, color: Colors.green), onPressed: () => _playDownloadedFile(item), ); case TaskStatus.running: return Stack( alignment: Alignment.center, children: [ CircularProgressIndicator(value: progress), IconButton( icon: const Icon(Icons.pause), onPressed: () => _downloadService?.pauseDownload( DownloadTask( taskId: 'download_$index', url: item.url!, filename: _getFileName(item), ), ), ), ], ); case TaskStatus.paused: return IconButton( icon: const Icon(Icons.play_arrow), onPressed: () => _downloadService?.resumeDownload( DownloadTask( taskId: 'download_$index', url: item.url!, filename: _getFileName(item), ), ), ); default: return IconButton( icon: const Icon(Icons.download), onPressed: () => _startDownload(item, index), ); } } Future _playDownloadedFile(VideoStream item) async {} @override Widget build(BuildContext context) { print("Neo"); return StreamBuilder( stream: _stream, builder: (context, snapshot) { if (snapshot.hasError) { return Text("Something went wrong: ${snapshot.error}"); } if (!snapshot.hasData && snapshot.connectionState == ConnectionState.done) { return const Center( child: Text("No streams available"), ); } if (!snapshot.hasData) { return const Center( child: CircularProgressIndicator(), ); } if (snapshot.data?.isEmpty == true) { return const Center( child: Text("No streams available"), ); } return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (ctx, index) { final item = snapshot.data![index]; return ListTile( onTap: () { DocSource? source; if ((item.behaviorHints)?.containsKey("iframe") == true) { final url = (item.behaviorHints!["iframe"] as String).replaceAll( "{imdb}", widget.item.imdbId!, ); source = IframeSource( url: url, title: widget.item.name!, id: widget.item.id, season: widget.season, episode: widget.episode, ); } if (item.infoHash != null) { source = TorrentSource( id: widget.item.id, title: widget.item.name!, infoHash: item.infoHash!, fileName: "${item.behaviorHints?["filename"] as String}.mp4", season: widget.season, episode: widget.episode, ); } if (item.url != null) { source = URLSource( title: "${utf8.decode( widget.item.name!.runes.toList(), )}.mp4", url: item.url!, id: widget.item.id, fileName: "${_getFileName(item)}.mp4", season: widget.season, episode: widget.episode, ); } if (source == null) { return; } Navigator.of(context).push( MaterialPageRoute(builder: (ctx) { return DocViewer( source: source!, ); }), ); }, enabled: item.behaviorHints?["filename"] != null, leading: const Icon(Icons.stream), title: Text( utf8.decode((item.name ?? item.title ?? "").runes.toList()), ), subtitle: Text( utf8.decode( (item.description ?? item.title ?? '').runes.toList()), ), trailing: _buildDownloadButton(item, index), ); }, ); }, ); } }