Skip to content

Commit 14c7e22

Browse files
committed
feat: improve offline playlist functionality and proxy handling
1 parent 98bd0a9 commit 14c7e22

4 files changed

Lines changed: 110 additions & 74 deletions

File tree

lib/screens/playlist_page.dart

Lines changed: 74 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -511,75 +511,84 @@ class _PlaylistPageState extends State<PlaylistPage> {
511511
return const SizedBox.shrink();
512512
}
513513

514-
return ValueListenableBuilder<List<dynamic>>(
515-
valueListenable: offlinePlaylistService.offlinePlaylists,
516-
builder: (context, offlinePlaylists, _) {
517-
playlistOfflineStatus = offlinePlaylistService.isPlaylistDownloaded(
518-
playlistId,
519-
);
520-
521-
if (playlistOfflineStatus) {
522-
return IconButton.filled(
523-
icon: Icon(
524-
FluentIcons.arrow_download_off_24_filled,
525-
color: Theme.of(context).colorScheme.onPrimary,
526-
),
527-
iconSize: 24,
528-
onPressed: () => _showRemoveOfflineDialog(playlistId),
529-
tooltip: context.l10n!.removeOffline,
530-
);
531-
}
532-
533-
return ValueListenableBuilder<DownloadProgress>(
534-
valueListenable: offlinePlaylistService.getProgressNotifier(
535-
playlistId,
536-
),
537-
builder: (context, progress, _) {
538-
final isDownloading = offlinePlaylistService.isPlaylistDownloading(
539-
playlistId,
540-
);
541-
542-
if (isDownloading) {
543-
return SizedBox(
544-
width: 48,
545-
height: 48,
546-
child: Stack(
547-
alignment: Alignment.center,
548-
children: [
549-
SizedBox(
550-
width: 40,
551-
height: 40,
552-
child: CircularProgressIndicator(
553-
value: progress.progress,
554-
strokeWidth: 3,
555-
backgroundColor: Theme.of(
556-
context,
557-
).colorScheme.primaryContainer,
558-
),
559-
),
560-
IconButton(
561-
icon: const Icon(FluentIcons.dismiss_24_filled, size: 16),
562-
onPressed: () => offlinePlaylistService.cancelDownload(
563-
context,
564-
playlistId,
565-
),
566-
tooltip: context.l10n!.cancel,
567-
),
568-
],
514+
return ValueListenableBuilder<int>(
515+
valueListenable: currentOfflineSongsLength,
516+
builder: (context, _, __) {
517+
return ValueListenableBuilder<List<dynamic>>(
518+
valueListenable: offlinePlaylistService.offlinePlaylists,
519+
builder: (context, offlinePlaylists, _) {
520+
final playlistSongs = _playlist?['list'] as List? ?? [];
521+
playlistOfflineStatus = isPlaylistFullyOffline(playlistSongs);
522+
523+
if (playlistOfflineStatus) {
524+
return IconButton.filled(
525+
icon: Icon(
526+
FluentIcons.arrow_download_off_24_filled,
527+
color: Theme.of(context).colorScheme.onPrimary,
569528
),
529+
iconSize: 24,
530+
onPressed: () => _showRemoveOfflineDialog(playlistId),
531+
tooltip: context.l10n!.removeOffline,
570532
);
571533
}
572534

573-
if (offlineMode.value) {
574-
return const SizedBox.shrink();
575-
}
576-
577-
return IconButton.filledTonal(
578-
icon: const Icon(FluentIcons.arrow_download_24_filled),
579-
iconSize: 24,
580-
onPressed: () =>
581-
offlinePlaylistService.downloadPlaylist(context, _playlist),
582-
tooltip: context.l10n!.downloadPlaylist,
535+
return ValueListenableBuilder<DownloadProgress>(
536+
valueListenable: offlinePlaylistService.getProgressNotifier(
537+
playlistId,
538+
),
539+
builder: (context, progress, _) {
540+
final isDownloading =
541+
offlinePlaylistService.isPlaylistDownloading(playlistId);
542+
543+
if (isDownloading) {
544+
return SizedBox(
545+
width: 48,
546+
height: 48,
547+
child: Stack(
548+
alignment: Alignment.center,
549+
children: [
550+
SizedBox(
551+
width: 40,
552+
height: 40,
553+
child: CircularProgressIndicator(
554+
value: progress.progress,
555+
strokeWidth: 3,
556+
backgroundColor: Theme.of(
557+
context,
558+
).colorScheme.primaryContainer,
559+
),
560+
),
561+
IconButton(
562+
icon: const Icon(
563+
FluentIcons.dismiss_24_filled,
564+
size: 16,
565+
),
566+
onPressed: () =>
567+
offlinePlaylistService.cancelDownload(
568+
context,
569+
playlistId,
570+
),
571+
tooltip: context.l10n!.cancel,
572+
),
573+
],
574+
),
575+
);
576+
}
577+
578+
if (offlineMode.value) {
579+
return const SizedBox.shrink();
580+
}
581+
582+
return IconButton.filledTonal(
583+
icon: const Icon(FluentIcons.arrow_download_24_filled),
584+
iconSize: 24,
585+
onPressed: () => offlinePlaylistService.downloadPlaylist(
586+
context,
587+
_playlist,
588+
),
589+
tooltip: context.l10n!.downloadPlaylist,
590+
);
591+
},
583592
);
584593
},
585594
);

lib/services/common_services.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,12 @@ bool isPlaylistAlreadyLiked(playlistIdToCheck) =>
286286
bool isSongAlreadyOffline(songIdToCheck) =>
287287
userOfflineSongs.any((song) => song['ytid'] == songIdToCheck);
288288

289+
bool isPlaylistFullyOffline(List songs) {
290+
if (songs.isEmpty) return false;
291+
final offlineIds = userOfflineSongs.map((s) => s['ytid']).toSet();
292+
return songs.every((s) => offlineIds.contains(s['ytid']));
293+
}
294+
289295
Map<String, dynamic> getOfflineSongByYtid(String ytid) {
290296
try {
291297
final song = userOfflineSongs.firstWhere(
@@ -337,7 +343,7 @@ Future<List<String>> getSearchSuggestions(String query) async {
337343

338344
Future<List<Map<String, int>>> getSkipSegments(String id) async {
339345
try {
340-
final res = await http.get(
346+
final res = await ProxyManager().getProxiedResponse(
341347
Uri(
342348
scheme: 'https',
343349
host: 'sponsor.ajay.app',
@@ -356,7 +362,7 @@ Future<List<Map<String, int>>> getSkipSegments(String id) async {
356362
},
357363
),
358364
);
359-
if (res.body != 'Not Found') {
365+
if (res.statusCode == 200 && res.body != 'Not Found') {
360366
final data = jsonDecode(res.body);
361367
final segments = data.map((obj) {
362368
return Map.castFrom<String, dynamic, String, int>({
@@ -667,7 +673,7 @@ Future<bool> removeSongFromOffline(dynamic songId) async {
667673

668674
Future<File?> _downloadAndSaveArtworkFile(String url, String filePath) async {
669675
try {
670-
final response = await http.get(Uri.parse(url));
676+
final response = await ProxyManager().getProxiedResponse(Uri.parse(url));
671677

672678
if (response.statusCode == 200) {
673679
final file = File(filePath);

lib/services/playlist_download_service.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,6 @@ class OfflinePlaylistService {
8484
return;
8585
}
8686

87-
// Check if already downloaded
88-
if (isPlaylistDownloaded(playlistId)) {
89-
showToast(context, context.l10n!.playlistAlreadyDownloaded);
90-
return;
91-
}
92-
9387
// Initialize download state
9488
final songsList = playlist['list'] as List<dynamic>? ?? [];
9589
if (songsList.isEmpty) {

lib/services/proxy_manager.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,33 @@ class ProxyManager {
395395
return manifest;
396396
}
397397

398+
/// Performs an HTTP GET request that respects current proxy settings.
399+
Future<http.Response> getProxiedResponse(
400+
Uri uri, {
401+
Map<String, String>? headers,
402+
int timeoutSeconds = 10,
403+
}) async {
404+
if (!useProxy.value || _sharedProxyAddress == null) {
405+
return http.get(uri, headers: headers).timeout(
406+
Duration(seconds: timeoutSeconds),
407+
onTimeout: () => http.Response('Timeout', 408),
408+
);
409+
}
410+
411+
final res = _proxyResources[_sharedProxyAddress!];
412+
if (res == null) {
413+
return http.get(uri, headers: headers).timeout(
414+
Duration(seconds: timeoutSeconds),
415+
onTimeout: () => http.Response('Timeout', 408),
416+
);
417+
}
418+
419+
return res.ioClient.get(uri, headers: headers).timeout(
420+
Duration(seconds: timeoutSeconds),
421+
onTimeout: () => http.Response('Timeout', 408),
422+
);
423+
}
424+
398425
void _closeAllProxyResources() {
399426
for (final res in _proxyResources.values) {
400427
try {

0 commit comments

Comments
 (0)