Skip to content

Commit

Permalink
feat: open-able external youtube playlists
Browse files Browse the repository at this point in the history
with default actions (download, play, addToPlaylist, alwaysAsk)
  • Loading branch information
MSOB7YY committed Nov 27, 2023
1 parent 1c4e022 commit 513a791
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 150 deletions.
4 changes: 4 additions & 0 deletions lib/core/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ final RegExp kYoutubeRegex = RegExp(
r'\b(?:https?://)?(?:www\.)?(?:youtube\.com/watch\?v=|youtu\.be/)([\w\-]+)(?:\S+)?',
caseSensitive: false,
);
final RegExp kYoutubeRegexPlaylists = RegExp(
r'\b(?:https?://)?(?:www\.)?(?:youtube\.com/playlist\?list=|youtu\.be/)([\w\-]+)(?:\S+)?',
caseSensitive: false,
);

/// Main Color
const Color kMainColor = Color.fromARGB(160, 117, 128, 224);
Expand Down
86 changes: 56 additions & 30 deletions lib/core/namida_converter_ext.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ import 'package:namida/youtube/controller/youtube_controller.dart';
import 'package:namida/youtube/controller/youtube_history_controller.dart';
import 'package:namida/youtube/functions/add_to_playlist_sheet.dart';
import 'package:namida/youtube/functions/download_sheet.dart';
import 'package:namida/youtube/functions/yt_playlist_utils.dart';
import 'package:namida/youtube/pages/youtube_home_view.dart';
import 'package:namida/youtube/pages/yt_history_page.dart';
import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart';
import 'package:namida/youtube/pages/yt_playlist_subpage.dart';
import 'package:namida/youtube/youtube_playlists_view.dart';

Expand Down Expand Up @@ -515,6 +517,29 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction {
String toText() => _NamidaConverters.inst.getTitle(this);
IconData toIcon() => _NamidaConverters.inst.getIcon(this);

Future<void> executePlaylist(String playlistUrl, {YoutubePlaylist? playlist, required BuildContext? context}) async {
final plInfo = playlist ?? await YoutubeController.inst.getPlaylistInfo(playlistUrl);
if (plInfo == null) return snackyy(title: lang.ERROR, message: 'error retrieving playlist info, check your connection?');
final didFetch = await plInfo.fetchAllPlaylistStreams(context: context?.mounted == true ? context : null);
if (!didFetch) return snackyy(title: lang.ERROR, message: 'error fetching playlist videos');

final streams = plInfo.streams;

switch (this) {
case OnYoutubeLinkOpenAction.showDownload:
plInfo.showPlaylistDownloadSheet(context: context?.mounted == true ? context : null);
case OnYoutubeLinkOpenAction.addToPlaylist:
showAddToPlaylistSheet(ids: streams.map((e) => e.id ?? ''), idsNamesLookup: {});
case OnYoutubeLinkOpenAction.play:
await Player.inst.playOrPause(0, streams.map((e) => YoutubeID(id: e.id ?? '', playlistID: null)), QueueSource.others);
case OnYoutubeLinkOpenAction.alwaysAsk:
_showAskDialog((action) => action.executePlaylist(playlistUrl, context: context, playlist: plInfo));

default:
null;
}
}

Future<void> execute(Iterable<String> ids) async {
switch (this) {
case OnYoutubeLinkOpenAction.showDownload:
Expand All @@ -523,50 +548,51 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction {
} else {
NamidaNavigator.inst.navigateTo(
YTPlaylistDownloadPage(
ids: ids.toList(),
ids: ids.map((e) => YoutubeID(id: e, playlistID: null)).toList(),
playlistName: 'External - ${DateTime.now().millisecondsSinceEpoch.dateAndClockFormattedOriginal}',
infoLookup: const {},
),
);
}
case OnYoutubeLinkOpenAction.addToPlaylist:
final idnames = {for (final id in ids) id: YoutubeController.inst.getBackupVideoInfo(id)?.title ?? YoutubeController.inst.fetchVideoDetailsFromCacheSync(id)?.name};
showAddToPlaylistSheet(ids: ids, idsNamesLookup: idnames);
showAddToPlaylistSheet(ids: ids, idsNamesLookup: {});
case OnYoutubeLinkOpenAction.play:
await Player.inst.playOrPause(0, ids.map((e) => YoutubeID(id: e, playlistID: null)), QueueSource.others);
case OnYoutubeLinkOpenAction.alwaysAsk:
{
final newVals = List<OnYoutubeLinkOpenAction>.from(OnYoutubeLinkOpenAction.values);
newVals.remove(OnYoutubeLinkOpenAction.alwaysAsk);
NamidaNavigator.inst.navigateDialog(
dialog: CustomBlurryDialog(
title: lang.CHOOSE,
normalTitleStyle: true,
actions: [
NamidaButton(
text: lang.DONE,
onPressed: NamidaNavigator.inst.closeDialog,
)
],
child: Column(
children: newVals
.map(
(e) => CustomListTile(
icon: e.toIcon(),
title: e.toText(),
onTap: () => e.execute(ids),
),
)
.toList(),
),
),
);
}
_showAskDialog((action) => action.execute(ids));

default:
null;
}
}

void _showAskDialog(void Function(OnYoutubeLinkOpenAction action) onTap) {
final newVals = List<OnYoutubeLinkOpenAction>.from(OnYoutubeLinkOpenAction.values);
newVals.remove(OnYoutubeLinkOpenAction.alwaysAsk);
NamidaNavigator.inst.navigateDialog(
dialog: CustomBlurryDialog(
title: lang.CHOOSE,
normalTitleStyle: true,
actions: [
NamidaButton(
text: lang.DONE,
onPressed: NamidaNavigator.inst.closeDialog,
)
],
child: Column(
children: newVals
.map(
(e) => CustomListTile(
icon: e.toIcon(),
title: e.toText(),
onTap: () => onTap(e),
),
)
.toList(),
),
),
);
}
}

extension PerformanceModeUtils on PerformanceMode {
Expand Down
8 changes: 6 additions & 2 deletions lib/youtube/controller/youtube_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,17 @@ class YoutubeController {
}

/// For full list of items, use [streams] getter in [playlist].
Future<List<StreamInfoItem>> getPlaylistStreams(YoutubePlaylist? playlist) async {
Future<List<StreamInfoItem>> getPlaylistStreams(YoutubePlaylist? playlist, {bool forceInitial = false}) async {
if (playlist == null) return [];
final streams = await playlist.getStreamsNextPage();
final streams = forceInitial ? await playlist.getStreams() : await playlist.getStreamsNextPage();
_fillTempVideoInfoMap(streams);
return streams;
}

Future<YoutubePlaylist?> getPlaylistInfo(String playlistUrl, {bool forceInitial = false}) async {
return await NewPipeExtractorDart.playlists.getPlaylistDetails(playlistUrl);
}

Future<void> _fetchComments(String id, {bool forceRequest = false}) async {
currentTotalCommentsCount.value = null;
currentComments.clear();
Expand Down
128 changes: 128 additions & 0 deletions lib/youtube/functions/yt_playlist_utils.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:newpipeextractor_dart/newpipeextractor_dart.dart' as yt;
import 'package:playlist_manager/module/playlist_id.dart';

import 'package:namida/controller/current_color.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/icon_fonts/broken_icons.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/packages/three_arched_circle.dart';
import 'package:namida/ui/dialogs/edit_tags_dialog.dart';
import 'package:namida/ui/widgets/custom_widgets.dart';
import 'package:namida/youtube/class/youtube_id.dart';
import 'package:namida/youtube/controller/youtube_controller.dart';
import 'package:namida/youtube/controller/youtube_playlist_controller.dart';
import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart';

extension YoutubePlaylistShare on YoutubePlaylist {
Future<void> shareVideos() async => await tracks.shareVideos();
Expand Down Expand Up @@ -113,3 +120,124 @@ extension YoutubePlaylistShare on YoutubePlaylist {
return controller.text;
}
}

extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist {
/// Sending a [context] means showing a bottom sheet with progress.
///
/// Returns wether the fetching process ended successfully, videos are accessible through [streams] getter.
Future<bool> fetchAllPlaylistStreams({required BuildContext? context}) async {
final playlist = this;
if (playlist.streams.length == playlist.streamCount) return true;

final currentCount = playlist.streams.length.obs;
final totalCount = playlist.streamCount.obs;
const switchAnimationDur = Duration(milliseconds: 600);
const switchAnimationDurHalf = Duration(milliseconds: 300);

bool isTotalCountNull() => totalCount.value < 0;

if (context != null) {
await Future.delayed(Duration.zero);
// ignore: use_build_context_synchronously
showModalBottomSheet(
context: context,
useRootNavigator: true,
isDismissible: false,
builder: (context) {
final iconSize = context.width * 0.5;
final iconColor = context.theme.colorScheme.onBackground.withOpacity(0.6);
return SizedBox(
width: context.width,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(
() => AnimatedSwitcher(
key: const Key('circle_switch'),
duration: switchAnimationDurHalf,
child: currentCount.value < totalCount.value || isTotalCountNull()
? ThreeArchedCircle(
size: iconSize,
color: iconColor,
)
: Icon(
key: const Key('tick_switch'),
Broken.tick_circle,
size: iconSize,
color: iconColor,
),
),
),
const SizedBox(height: 12.0),
Text(
'${lang.FETCHING}...',
style: context.textTheme.displayLarge,
),
const SizedBox(height: 8.0),
Obx(
() => Text(
'${currentCount.value.formatDecimal()}/${isTotalCountNull() ? '?' : totalCount.value.formatDecimal()}',
style: context.textTheme.displayLarge,
),
),
],
),
),
);
},
);
}

if (isTotalCountNull() || currentCount.value == 0) {
await YoutubeController.inst.getPlaylistStreams(playlist, forceInitial: currentCount.value == 0);
currentCount.value = playlist.streams.length;
totalCount.value = playlist.streamCount < 0 ? playlist.streams.length : playlist.streamCount;
}

// -- if still not fetched
if (isTotalCountNull()) {
if (context != null && context.mounted) Navigator.of(context).pop();
currentCount.close();
return false;
}

while (currentCount.value < totalCount.value) {
final res = await YoutubeController.inst.getPlaylistStreams(playlist);
if (res.isEmpty) break;
currentCount.value = playlist.streams.length;
}

if (context != null) {
await Future.delayed(switchAnimationDur);
if (context.mounted) Navigator.of(context).pop();
}

currentCount.close();
totalCount.close();
return true;
}

Future<void> showPlaylistDownloadSheet({required BuildContext? context}) async {
final didFetch = await fetchAllPlaylistStreams(context: context);
if (!didFetch) return snackyy(title: lang.ERROR, message: 'error fetching playlist videos');
final playlist = this;
final plID = playlist.id;
final videoIDs = playlist.streams.map((e) => YoutubeID(
id: e.id ?? '',
playlistID: plID == null ? null : PlaylistID(id: plID),
));
final infoLookup = <String, yt.StreamInfoItem>{};
playlist.streams.loop((e, index) {
infoLookup[e.id ?? ''] = e;
});
NamidaNavigator.inst.navigateTo(
YTPlaylistDownloadPage(
ids: videoIDs.toList(),
playlistName: playlist.name ?? '',
infoLookup: infoLookup,
),
);
}
}
20 changes: 10 additions & 10 deletions lib/youtube/pages/yt_playlist_download_subpage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ class _YTPlaylistDownloadPageState extends State<YTPlaylistDownloadPage> {
12.0;
}

double hmultiplier = 0.7;
double previousScale = 0.7;
double _hmultiplier = 0.7;
double _previousScale = 0.7;

@override
Widget build(BuildContext context) {
final thumWidth = context.width * 0.3 * hmultiplier;
final thumWidth = context.width * 0.3 * _hmultiplier;
final thumHeight = thumWidth * 9 / 16;
return BackgroundWrapper(
child: Stack(
Expand Down Expand Up @@ -286,7 +286,7 @@ class _YTPlaylistDownloadPageState extends State<YTPlaylistDownloadPage> {
slivers: [
const SliverPadding(padding: EdgeInsets.only(bottom: 12.0)),
SliverFixedExtentList.builder(
itemExtent: Dimensions.youtubeCardItemExtent * hmultiplier,
itemExtent: Dimensions.youtubeCardItemExtent * _hmultiplier,
itemCount: widget.ids.length,
itemBuilder: (context, index) {
final id = widget.ids[index].id;
Expand All @@ -303,8 +303,8 @@ class _YTPlaylistDownloadPageState extends State<YTPlaylistDownloadPage> {
final fileExists = File("${AppDirs.YOUTUBE_DOWNLOADS}${_groupName.value}/$filename").existsSync();
return NamidaInkWell(
animationDurationMS: 200,
height: Dimensions.youtubeCardItemHeight * hmultiplier,
margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: Dimensions.youtubeCardItemVerticalPadding * hmultiplier),
height: Dimensions.youtubeCardItemHeight * _hmultiplier,
margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: Dimensions.youtubeCardItemVerticalPadding * _hmultiplier),
borderRadius: 12.0,
bgColor: context.theme.cardColor.withOpacity(0.3),
decoration: isSelected
Expand Down Expand Up @@ -363,7 +363,7 @@ class _YTPlaylistDownloadPageState extends State<YTPlaylistDownloadPage> {
const SizedBox(height: 6.0),
Text(
info?.name ?? id,
style: context.textTheme.displayMedium?.copyWith(fontSize: 15.0.multipliedFontScale * hmultiplier),
style: context.textTheme.displayMedium?.copyWith(fontSize: 15.0.multipliedFontScale * _hmultiplier),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Expand All @@ -378,7 +378,7 @@ class _YTPlaylistDownloadPageState extends State<YTPlaylistDownloadPage> {
const SizedBox(width: 2.0),
Text(
info?.uploaderName ?? '',
style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale * hmultiplier),
style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale * _hmultiplier),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Expand Down Expand Up @@ -512,8 +512,8 @@ class _YTPlaylistDownloadPageState extends State<YTPlaylistDownloadPage> {
),
Positioned.fill(
child: GestureDetector(
onScaleStart: (details) => previousScale = hmultiplier,
onScaleUpdate: (details) => setState(() => hmultiplier = (details.scale * previousScale).clamp(0.5, 2.0)),
onScaleStart: (details) => _previousScale = _hmultiplier,
onScaleUpdate: (details) => setState(() => _hmultiplier = (details.scale * _previousScale).clamp(0.5, 2.0)),
),
),
],
Expand Down

0 comments on commit 513a791

Please sign in to comment.