Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/config/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ class _ScaffoldWithNav extends ConsumerWidget {
label: 'Discover',
),
BottomNavigationBarItem(
icon: Icon(Icons.download_outlined),
activeIcon: Icon(Icons.download),
label: 'Downloads',
icon: Icon(Icons.offline_pin_outlined),
activeIcon: Icon(Icons.offline_pin),
label: 'Offline',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
Expand Down
23 changes: 0 additions & 23 deletions lib/providers/download_progress_provider.dart

This file was deleted.

11 changes: 2 additions & 9 deletions lib/providers/websocket_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import '../services/offline_service.dart';
import '../services/api_service.dart';
import 'auth_provider.dart';
import 'channel_provider.dart';
import 'download_progress_provider.dart';
import 'feed_provider.dart';
import 'discover_provider.dart';
import 'offline_provider.dart';
Expand Down Expand Up @@ -43,18 +42,12 @@ final webSocketConnectionProvider = Provider<void>((ref) {
final subscription = wsService.events.listen((event) {
switch (event.type) {
case WebSocketEventType.downloadProgress:
final videoId = event.data['video_id'] as String?;
final pct = (event.data['percentage'] as num?)?.toDouble();
if (videoId != null && pct != null) {
ref
.read(downloadProgressProvider.notifier)
.updateProgress(videoId, pct);
}
// Caching is invisible — download progress is no longer surfaced.
break;
case WebSocketEventType.downloadComplete:
final videoId = event.data['video_id'] as String?;
final channelId = event.data['channel_id'] as String?;
if (videoId != null) {
ref.read(downloadProgressProvider.notifier).removeProgress(videoId);
ref.invalidate(videoDetailProvider(videoId));
}
if (channelId != null) {
Expand Down
132 changes: 11 additions & 121 deletions lib/screens/channel_detail_screen.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/video.dart';
import '../providers/channel_provider.dart';
import '../providers/download_progress_provider.dart';
import '../providers/feed_provider.dart';
import '../providers/settings_provider.dart';
import '../services/api_service.dart';
import '../services/storage_service.dart';
import '../widgets/queue_action.dart';
Expand All @@ -26,9 +22,6 @@ class ChannelDetailScreen extends ConsumerStatefulWidget {
}

class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
Timer? _pollTimer;
final Set<String> _pendingVideoIds = {};

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -62,52 +55,6 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
}
}

@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}

void _startPolling() {
if (_pollTimer?.isActive ?? false) return;
_pollTimer = Timer.periodic(const Duration(seconds: 5), (_) {
ref.invalidate(channelVideosProvider(widget.channelId));
});
}

void _stopPolling() {
_pollTimer?.cancel();
_pollTimer = null;
}

void _checkPollingNeeded(List<Video> videos) {
for (final id in _pendingVideoIds.toList()) {
final video = videos.where((v) => v.id == id).firstOrNull;
if (video != null && video.status != VideoStatus.cataloged) {
_pendingVideoIds.remove(id);
}
}

final hasInProgress =
videos.any((v) => v.isInProgress) || _pendingVideoIds.isNotEmpty;
if (hasInProgress) {
_startPolling();
} else {
_stopPolling();
}
}

List<Video> _applyOptimisticUpdates(List<Video> videos) {
if (_pendingVideoIds.isEmpty) return videos;
return videos.map((v) {
if (_pendingVideoIds.contains(v.id) &&
v.status == VideoStatus.cataloged) {
return v.copyWith(status: VideoStatus.pending);
}
return v;
}).toList();
}

void _invalidateChannel() {
ref.invalidate(channelDetailProvider(widget.channelId));
ref.invalidate(channelVideosProvider(widget.channelId));
Expand Down Expand Up @@ -179,11 +126,8 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
Widget build(BuildContext context) {
final channelAsync = ref.watch(channelDetailProvider(widget.channelId));
final videosAsync = ref.watch(channelVideosProvider(widget.channelId));
final progressMap = ref.watch(downloadProgressProvider);
final padding = AdaptiveLayout.contentPadding(context);

videosAsync.whenData(_checkPollingNeeded);

return Scaffold(
// The loaded state brings its own SliverAppBar; loading/error states
// still need a back button.
Expand Down Expand Up @@ -352,7 +296,7 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
),
videosAsync.when(
data: (videos) {
final displayVideos = _applyOptimisticUpdates(videos);
final displayVideos = videos;
if (displayVideos.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
Expand All @@ -371,23 +315,16 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
final video = displayVideos[index];
return VideoListTile(
video: video,
downloadProgress: progressMap[video.id],
onTap: (video.isPlayable || video.isInProgress)
? () async {
await context.push('/player/${video.id}');
if (!mounted) return;
ref.invalidate(
channelVideosProvider(widget.channelId),
);
invalidateFeedProviders(ref);
}
: null,
onDownload: video.isDownloadable
? () => _onDownload(video)
: null,
onCancel: video.isInProgress
? () => _onCancelDownload(video)
: null,
// Every episode plays on tap — cached or not (an
// un-cached one starts via instant-stream).
onTap: () async {
await context.push('/player/${video.id}');
if (!mounted) return;
ref.invalidate(
channelVideosProvider(widget.channelId),
);
invalidateFeedProviders(ref);
},
onMenu: () => _showVideoMenu(video),
);
}, childCount: displayVideos.length),
Expand Down Expand Up @@ -485,16 +422,6 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
video: video,
onTap: () => Navigator.pop(sheetContext),
),
if (video.status == VideoStatus.complete)
ListTile(
leading: const Icon(Icons.download_rounded),
title: const Text('Re-download'),
subtitle: const Text('Fetch the video from YouTube again'),
onTap: () {
Navigator.pop(sheetContext);
_onDownload(video);
},
),
ListTile(
leading: const Icon(
Icons.delete_outline,
Expand Down Expand Up @@ -553,41 +480,4 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
).showSnackBar(SnackBar(content: Text(e.message)));
}
}

Future<void> _onCancelDownload(Video video) async {
final api = ref.read(apiServiceProvider);
try {
await api.cancelDownload(video.id);
ref.invalidate(channelVideosProvider(widget.channelId));
} on ApiException catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to cancel download: ${e.message}')),
);
}
}
}

Future<void> _onDownload(Video video) async {
final api = ref.read(apiServiceProvider);
final quality = ref.read(settingsProvider).preferredQuality;

setState(() {
_pendingVideoIds.add(video.id);
});

try {
await api.downloadVideo(video.id, quality: quality);
if (!mounted) return;
_startPolling();
} on ApiException catch (e) {
if (!mounted) return;
setState(() {
_pendingVideoIds.remove(video.id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to start download: ${e.message}')),
);
}
}
}
Loading
Loading