Show loading when action takes some time
Convert RxJava3 calls to suspend functions, sometimes requiring .await() to bridge between the two Also migrate play queue items' popup menu to new long press menu Also do centralized error handling
This commit is contained in:
parent
c62004d903
commit
8f19f95fee
@ -1,94 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.SparseItemUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
private QueueItemMenuUtil() {
|
||||
}
|
||||
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
final boolean hideDetails,
|
||||
final FragmentManager fragmentManager,
|
||||
final Context context) {
|
||||
final ContextThemeWrapper themeWrapper =
|
||||
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||
|
||||
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||
|
||||
if (hideDetails) {
|
||||
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_item_remove:
|
||||
final int index = playQueue.indexOf(item);
|
||||
playQueue.remove(index);
|
||||
return true;
|
||||
case R.id.menu_item_details:
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||
item.getUrl(), item.getTitle(), null,
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
List.of(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragmentManager,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
case R.id.menu_item_channel_details:
|
||||
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||
item.getUrl(), item.getUploaderUrl(),
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||
));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnails());
|
||||
return true;
|
||||
case R.id.menu_item_download:
|
||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||
info -> {
|
||||
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
||||
info);
|
||||
downloadDialog.show(fragmentManager, "downloadDialog");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
}
|
||||
@ -823,15 +823,15 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.compose(this::pleaseWait)
|
||||
.subscribe(
|
||||
info -> getActivityContext().ifPresent(context ->
|
||||
PlaylistDialog.createCorrespondingDialog(context,
|
||||
List.of(new StreamEntity(info)),
|
||||
playlistDialog -> runOnVisible(ctx -> {
|
||||
// dismiss listener to be handled by FragmentManager
|
||||
final FragmentManager fm =
|
||||
ctx.getSupportFragmentManager();
|
||||
playlistDialog.show(fm, "addToPlaylistDialog");
|
||||
})
|
||||
)),
|
||||
disposables.add(
|
||||
PlaylistDialog.createCorrespondingDialog(context,
|
||||
List.of(new StreamEntity(info)))
|
||||
.subscribe(dialog -> runOnVisible(ctx -> {
|
||||
// dismiss listener to be handled by FragmentManager
|
||||
final FragmentManager fm =
|
||||
ctx.getSupportFragmentManager();
|
||||
dialog.show(fm, "addToPlaylistDialog");
|
||||
})))),
|
||||
throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
|
||||
throwable, UserAction.REQUESTED_STREAM,
|
||||
"Tried to add " + currentUrl + " to a playlist",
|
||||
|
||||
@ -37,8 +37,8 @@ enum class UserAction(val message: String) {
|
||||
PREFERENCES_MIGRATION("migration of preferences"),
|
||||
SHARE_TO_NEWPIPE("share to newpipe"),
|
||||
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
|
||||
OPEN_INFO_ITEM_DIALOG("open info item dialog"),
|
||||
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
|
||||
PLAY_ON_POPUP("play on popup"),
|
||||
SUBSCRIPTIONS("loading subscriptions")
|
||||
SUBSCRIPTIONS("loading subscriptions"),
|
||||
LONG_PRESS_MENU_ACTION("long press menu action"),
|
||||
}
|
||||
|
||||
@ -439,7 +439,7 @@ class VideoDetailFragment :
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
requireContext(),
|
||||
listOf(StreamEntity(info))
|
||||
) { dialog -> dialog.show(getParentFragmentManager(), TAG) }
|
||||
).subscribe { dialog -> dialog.show(getParentFragmentManager(), TAG) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,15 +237,15 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
break;
|
||||
case R.id.menu_item_append_playlist:
|
||||
if (currentInfo != null) {
|
||||
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
getPlayQueue()
|
||||
.getStreams()
|
||||
.stream()
|
||||
.map(StreamEntity::new)
|
||||
.collect(Collectors.toList()),
|
||||
dialog -> dialog.show(getFM(), TAG)
|
||||
));
|
||||
disposables.add(
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
getPlayQueue()
|
||||
.getStreams()
|
||||
.stream()
|
||||
.map(StreamEntity::new)
|
||||
.collect(Collectors.toList())
|
||||
).subscribe(dialog -> dialog.show(getFM(), TAG)));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -20,11 +20,11 @@ import org.schabi.newpipe.util.StateSaver;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
|
||||
@ -135,22 +135,18 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
*
|
||||
* @param context context used for accessing the database
|
||||
* @param streamEntities used for crating the dialog
|
||||
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
||||
* @return the disposable that was created
|
||||
* @return the {@link Maybe} to subscribe to to obtain the correct {@link PlaylistDialog}
|
||||
*/
|
||||
public static Disposable createCorrespondingDialog(
|
||||
public static Maybe<PlaylistDialog> createCorrespondingDialog(
|
||||
final Context context,
|
||||
final List<StreamEntity> streamEntities,
|
||||
final Consumer<PlaylistDialog> onExec) {
|
||||
final List<StreamEntity> streamEntities) {
|
||||
|
||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||
.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(hasPlaylists ->
|
||||
onExec.accept(hasPlaylists
|
||||
? PlaylistAppendDialog.newInstance(streamEntities)
|
||||
: PlaylistCreationDialog.newInstance(streamEntities))
|
||||
);
|
||||
.map(hasPlaylists -> hasPlaylists
|
||||
? PlaylistAppendDialog.newInstance(streamEntities)
|
||||
: PlaylistCreationDialog.newInstance(streamEntities))
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,7 +171,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
return Disposable.empty();
|
||||
}
|
||||
|
||||
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
|
||||
dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
|
||||
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities)
|
||||
.subscribe(dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.ui.components.menu.LongPressMenuKt.openLongPressMenuInActivity;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
@ -41,6 +41,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressAction;
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressable;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
@ -328,8 +330,11 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
@Override
|
||||
public void held(final PlayQueueItem item, final View view) {
|
||||
if (player != null && player.getPlayQueue().indexOf(item) != -1) {
|
||||
openPopupMenu(player.getPlayQueue(), item, view, false,
|
||||
getSupportFragmentManager(), PlayQueueActivity.this);
|
||||
openLongPressMenuInActivity(
|
||||
PlayQueueActivity.this,
|
||||
LongPressable.fromPlayQueueItem(item),
|
||||
LongPressAction.fromPlayQueueItem(item, player.getPlayQueue(), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,15 @@ class PlayQueueItem private constructor(
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnError { throwable -> error = throwable }
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||
item.duration = duration
|
||||
item.thumbnails = thumbnails
|
||||
item.uploaderName = uploader
|
||||
item.uploaderUrl = uploaderUrl
|
||||
return item
|
||||
}
|
||||
|
||||
override fun equals(o: Any?) = o is PlayQueueItem && serviceId == o.serviceId && url == o.url
|
||||
|
||||
override fun hashCode() = Objects.hash(url, serviceId)
|
||||
|
||||
@ -2,7 +2,6 @@ package org.schabi.newpipe.player.ui;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
|
||||
@ -14,6 +13,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAct
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.ui.components.menu.LongPressMenuKt.openLongPressMenuInActivity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@ -68,6 +68,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressAction;
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressable;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
@ -795,8 +797,11 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
@Nullable final PlayQueue playQueue = player.getPlayQueue();
|
||||
@Nullable final AppCompatActivity parentActivity = getParentActivity().orElse(null);
|
||||
if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) {
|
||||
openPopupMenu(player.getPlayQueue(), item, view, true,
|
||||
parentActivity.getSupportFragmentManager(), context);
|
||||
openLongPressMenuInActivity(
|
||||
parentActivity,
|
||||
LongPressable.fromPlayQueueItem(item),
|
||||
LongPressAction.fromPlayQueueItem(item, playQueue, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,9 @@ import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.changedToUp
|
||||
import androidx.compose.ui.input.pointer.isOutOfBounds
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@ -87,3 +89,22 @@ fun Modifier.detectDragGestures(
|
||||
}
|
||||
|
||||
private fun Offset.toIntOffset() = IntOffset(this.x.toInt(), this.y.toInt())
|
||||
|
||||
/**
|
||||
* Discards all touches on child composables. See https://stackoverflow.com/a/69146178.
|
||||
* @param doDiscard whether this Modifier is active (touches discarded) or not (no effect).
|
||||
*/
|
||||
fun Modifier.discardAllTouchesIf(doDiscard: Boolean) = if (doDiscard) {
|
||||
pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
// we should wait for all new pointer events
|
||||
while (true) {
|
||||
awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
.changes
|
||||
.forEach(PointerInputChange::consume)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe.ui.components.menu
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||
@ -15,6 +15,7 @@ import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Headset
|
||||
import androidx.compose.material.icons.filled.HideImage
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.OpenInBrowser
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.PictureInPicture
|
||||
@ -23,7 +24,11 @@ import androidx.compose.material.icons.filled.QueuePlayNext
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.core.net.toUri
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import kotlinx.coroutines.rx3.awaitSingle
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||
@ -31,9 +36,6 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.download.DownloadDialog
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.InfoItem
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||
@ -42,21 +44,22 @@ import org.schabi.newpipe.ktx.findFragmentActivity
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager
|
||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
|
||||
import org.schabi.newpipe.ui.components.menu.icons.BackgroundFromHere
|
||||
import org.schabi.newpipe.ui.components.menu.icons.PlayFromHere
|
||||
import org.schabi.newpipe.ui.components.menu.icons.PopupFromHere
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.SparseItemUtil
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
|
||||
data class LongPressAction(
|
||||
val type: Type,
|
||||
val action: (context: Context) -> Unit,
|
||||
@MainThread
|
||||
val action: suspend (context: Context) -> Unit,
|
||||
val enabled: (isPlayerRunning: Boolean) -> Boolean = { true },
|
||||
) {
|
||||
enum class Type(
|
||||
@ -88,6 +91,8 @@ data class LongPressAction(
|
||||
SetAsPlaylistThumbnail(17, R.string.set_as_playlist_thumbnail, Icons.Default.Image),
|
||||
UnsetPlaylistThumbnail(18, R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
|
||||
Unsubscribe(19, R.string.unsubscribe, Icons.Default.Delete),
|
||||
ShowDetails(20, R.string.play_queue_stream_detail, Icons.Default.Info),
|
||||
Remove(21, R.string.play_queue_remove, Icons.Default.Delete),
|
||||
;
|
||||
|
||||
// TODO allow actions to return disposables
|
||||
@ -95,37 +100,39 @@ data class LongPressAction(
|
||||
|
||||
fun buildAction(
|
||||
enabled: (isPlayerRunning: Boolean) -> Boolean = { true },
|
||||
action: (context: Context) -> Unit,
|
||||
action: suspend (context: Context) -> Unit,
|
||||
) = LongPressAction(this, action, enabled)
|
||||
|
||||
companion object {
|
||||
// ShowChannelDetails is not enabled by default, since navigating to channel details can
|
||||
// also be done by clicking on the uploader name in the long press menu header
|
||||
val DefaultEnabledActions: List<Type> = listOf(
|
||||
Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere, Download,
|
||||
ShowDetails, Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere, Download,
|
||||
AddToPlaylist, Share, OpenInBrowser, MarkAsWatched, Delete,
|
||||
Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Unsubscribe
|
||||
Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Unsubscribe, Remove,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun buildPlayerActionList(queue: () -> PlayQueue): List<LongPressAction> {
|
||||
private fun buildPlayerActionList(
|
||||
queue: suspend (Context) -> PlayQueue
|
||||
): List<LongPressAction> {
|
||||
return listOf(
|
||||
Type.Enqueue.buildAction({ isPlayerRunning -> isPlayerRunning }) { context ->
|
||||
NavigationHelper.enqueueOnPlayer(context, queue())
|
||||
NavigationHelper.enqueueOnPlayer(context, queue(context))
|
||||
},
|
||||
Type.EnqueueNext.buildAction({ isPlayerRunning -> isPlayerRunning }) { context ->
|
||||
NavigationHelper.enqueueNextOnPlayer(context, queue())
|
||||
NavigationHelper.enqueueNextOnPlayer(context, queue(context))
|
||||
},
|
||||
Type.Background.buildAction { context ->
|
||||
NavigationHelper.playOnBackgroundPlayer(context, queue(), true)
|
||||
NavigationHelper.playOnBackgroundPlayer(context, queue(context), true)
|
||||
},
|
||||
Type.Popup.buildAction { context ->
|
||||
NavigationHelper.playOnPopupPlayer(context, queue(), true)
|
||||
NavigationHelper.playOnPopupPlayer(context, queue(context), true)
|
||||
},
|
||||
Type.Play.buildAction { context ->
|
||||
NavigationHelper.playOnMainPlayer(context, queue(), false)
|
||||
NavigationHelper.playOnMainPlayer(context, queue(context), false)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -166,6 +173,53 @@ data class LongPressAction(
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildAdditionalStreamActionList(item: StreamInfoItem): List<LongPressAction> {
|
||||
return listOf(
|
||||
Type.Download.buildAction { context ->
|
||||
val info = fetchStreamInfoAndSaveToDatabase(context, item.serviceId, item.url)
|
||||
val downloadDialog = DownloadDialog(context, info)
|
||||
val fragmentManager = context.findFragmentActivity()
|
||||
.supportFragmentManager
|
||||
downloadDialog.show(fragmentManager, "downloadDialog")
|
||||
},
|
||||
Type.AddToPlaylist.buildAction { context ->
|
||||
LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||
.hasPlaylists()
|
||||
val dialog = withContext(Dispatchers.IO) {
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
listOf(StreamEntity(item))
|
||||
)
|
||||
.awaitSingle()
|
||||
}
|
||||
val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
|
||||
dialog.show(
|
||||
context.findFragmentActivity().supportFragmentManager,
|
||||
"StreamDialogEntry@${tag}_playlist"
|
||||
)
|
||||
},
|
||||
Type.ShowChannelDetails.buildAction { context ->
|
||||
val uploaderUrl = fetchUploaderUrlIfSparse(
|
||||
context, item.serviceId, item.url, item.uploaderUrl
|
||||
)
|
||||
NavigationHelper.openChannelFragment(
|
||||
context.findFragmentActivity().supportFragmentManager,
|
||||
item.serviceId,
|
||||
uploaderUrl,
|
||||
item.uploaderName,
|
||||
)
|
||||
},
|
||||
Type.MarkAsWatched.buildAction { context ->
|
||||
withContext(Dispatchers.IO) {
|
||||
HistoryRecordManager(context).markAsWatched(item).await()
|
||||
}
|
||||
},
|
||||
Type.PlayWithKodi.buildAction { context ->
|
||||
KoreUtils.playWithKore(context, item.url.toUri())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param queueFromHere returns a play queue for the list that contains [item], with the
|
||||
* queue index pointing to [item], used to build actions like "Play playlist from here".
|
||||
@ -176,65 +230,10 @@ data class LongPressAction(
|
||||
queueFromHere: (() -> PlayQueue)?,
|
||||
/* TODO isKodiEnabled: Boolean, */
|
||||
): List<LongPressAction> {
|
||||
return buildPlayerActionList { SinglePlayQueue(item) } +
|
||||
return buildPlayerActionList { context -> fetchItemInfoIfSparse(context, item) } +
|
||||
(queueFromHere?.let { buildPlayerFromHereActionList(queueFromHere) } ?: listOf()) +
|
||||
buildShareActionList(item) +
|
||||
listOf(
|
||||
Type.Download.buildAction { context ->
|
||||
SparseItemUtil.fetchStreamInfoAndSaveToDatabase(
|
||||
context, item.serviceId, item.url
|
||||
) { info ->
|
||||
val downloadDialog = DownloadDialog(context, info)
|
||||
val fragmentManager = context.findFragmentActivity()
|
||||
.supportFragmentManager
|
||||
downloadDialog.show(fragmentManager, "downloadDialog")
|
||||
}
|
||||
},
|
||||
Type.AddToPlaylist.buildAction { context ->
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
listOf(StreamEntity(item))
|
||||
) { dialog: PlaylistDialog ->
|
||||
val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
|
||||
dialog.show(
|
||||
context.findFragmentActivity().supportFragmentManager,
|
||||
"StreamDialogEntry@${tag}_playlist"
|
||||
)
|
||||
}
|
||||
},
|
||||
Type.ShowChannelDetails.buildAction { context ->
|
||||
SparseItemUtil.fetchUploaderUrlIfSparse(
|
||||
context, item.serviceId, item.url, item.uploaderUrl
|
||||
) { url: String ->
|
||||
NavigationHelper.openChannelFragment(
|
||||
context.findFragmentActivity().supportFragmentManager,
|
||||
item.serviceId,
|
||||
url,
|
||||
item.uploaderName,
|
||||
)
|
||||
}
|
||||
},
|
||||
Type.MarkAsWatched.buildAction { context ->
|
||||
HistoryRecordManager(context)
|
||||
.markAsWatched(item)
|
||||
.doOnError { error ->
|
||||
ErrorUtil.showSnackbar(
|
||||
context,
|
||||
ErrorInfo(
|
||||
error,
|
||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||
"Got an error when trying to mark as watched"
|
||||
)
|
||||
)
|
||||
}
|
||||
.onErrorComplete()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe()
|
||||
},
|
||||
Type.PlayWithKodi.buildAction { context ->
|
||||
KoreUtils.playWithKore(context, item.url.toUri())
|
||||
},
|
||||
)
|
||||
buildAdditionalStreamActionList(item)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -248,6 +247,38 @@ data class LongPressAction(
|
||||
return fromStreamInfoItem(item.toStreamInfoItem(), queueFromHere)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fromPlayQueueItem(
|
||||
item: PlayQueueItem,
|
||||
playQueueFromWhichToDelete: PlayQueue,
|
||||
showDetails: Boolean,
|
||||
): List<LongPressAction> {
|
||||
// TODO decide if it's fine to just convert to StreamInfoItem here (it poses an
|
||||
// unnecessary dependency on the extractor, when we want to just look at data; maybe
|
||||
// using something like LongPressable would work)
|
||||
val streamInfoItem = item.toStreamInfoItem()
|
||||
return buildShareActionList(streamInfoItem) +
|
||||
buildAdditionalStreamActionList(streamInfoItem) +
|
||||
if (showDetails) {
|
||||
listOf(
|
||||
Type.ShowDetails.buildAction { context ->
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(
|
||||
context, item.serviceId, item.url, item.title, null, false
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
listOf()
|
||||
} +
|
||||
listOf(
|
||||
Type.Remove.buildAction {
|
||||
val index = playQueueFromWhichToDelete.indexOf(item)
|
||||
playQueueFromWhichToDelete.remove(index)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fromStreamStatisticsEntry(
|
||||
item: StreamStatisticsEntry,
|
||||
@ -256,16 +287,13 @@ data class LongPressAction(
|
||||
return fromStreamEntity(item.streamEntity, queueFromHere) +
|
||||
listOf(
|
||||
Type.Delete.buildAction { context ->
|
||||
HistoryRecordManager(context)
|
||||
.deleteStreamHistoryAndState(item.streamId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.one_item_deleted,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
HistoryRecordManager(context)
|
||||
.deleteStreamHistoryAndState(item.streamId)
|
||||
.await()
|
||||
}
|
||||
Toast.makeText(context, R.string.one_item_deleted, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,9 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@ -32,6 +35,7 @@ import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -48,6 +52,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -74,11 +79,18 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.UserAction.LONG_PRESS_MENU_ACTION
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.EnqueueNext
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.ShowChannelDetails
|
||||
import org.schabi.newpipe.ui.discardAllTouchesIf
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.ui.theme.customColors
|
||||
import org.schabi.newpipe.util.Either
|
||||
@ -129,9 +141,10 @@ fun LongPressMenu(
|
||||
val isHeaderEnabled by viewModel.isHeaderEnabled.collectAsState()
|
||||
val actionArrangement by viewModel.actionArrangement.collectAsState()
|
||||
var showEditor by rememberSaveable { mutableStateOf(false) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
if (showEditor) {
|
||||
if (showEditor && !isLoading) {
|
||||
// we can't put the editor in a bottom sheet, because it relies on dragging gestures
|
||||
Dialog(
|
||||
onDismissRequest = { showEditor = false },
|
||||
@ -153,19 +166,33 @@ fun LongPressMenu(
|
||||
}
|
||||
}
|
||||
|
||||
val ctx = LocalContext.current
|
||||
// run actions on the main thread!
|
||||
val coroutineScope = rememberCoroutineScope { Dispatchers.Main }
|
||||
fun runActionAndDismiss(action: LongPressAction) {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
action.action(ctx)
|
||||
} catch (t: Throwable) {
|
||||
ErrorUtil.showSnackbar(
|
||||
ctx, ErrorInfo(t, LONG_PRESS_MENU_ACTION, "Running action ${action.type}")
|
||||
)
|
||||
}
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
// show a clickable uploader in the header if an uploader action is available and the
|
||||
// "show channel details" action is not enabled as a standalone action
|
||||
val ctx = LocalContext.current
|
||||
val onUploaderClick by remember {
|
||||
derivedStateOf {
|
||||
longPressActions.firstOrNull { it.type == ShowChannelDetails }
|
||||
?.takeIf { !actionArrangement.contains(ShowChannelDetails) }
|
||||
?.let { showChannelDetailsAction ->
|
||||
{
|
||||
showChannelDetailsAction.action(ctx)
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
?.let { showChannelAction -> { runActionAndDismiss(showChannelAction) } }
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,12 +201,27 @@ fun LongPressMenu(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dragHandle = { LongPressMenuDragHandle(onEditActions = { showEditor = true }) },
|
||||
) {
|
||||
LongPressMenuContent(
|
||||
header = longPressable.takeIf { isHeaderEnabled },
|
||||
onUploaderClick = onUploaderClick,
|
||||
actions = enabledLongPressActions,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
Box(modifier = Modifier.discardAllTouchesIf(isLoading)) {
|
||||
LongPressMenuContent(
|
||||
header = longPressable.takeIf { isHeaderEnabled },
|
||||
onUploaderClick = onUploaderClick,
|
||||
actions = enabledLongPressActions,
|
||||
runActionAndDismiss = ::runActionAndDismiss,
|
||||
)
|
||||
// importing makes the ColumnScope overload be resolved, so we use qualified path...
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = isLoading,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,7 +231,7 @@ private fun LongPressMenuContent(
|
||||
header: LongPressable?,
|
||||
onUploaderClick: (() -> Unit)?,
|
||||
actions: List<LongPressAction>,
|
||||
onDismissRequest: () -> Unit,
|
||||
runActionAndDismiss: (LongPressAction) -> Unit,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
@ -203,7 +245,6 @@ private fun LongPressMenuContent(
|
||||
// width for the landscape/reduced header, measured in button widths
|
||||
val headerWidthInButtonsReducedSpan = 4
|
||||
val buttonsPerRow = (this.maxWidth / MinButtonWidth).toInt()
|
||||
val ctx = LocalContext.current
|
||||
|
||||
Column {
|
||||
var actionIndex = if (header != null) -1 else 0 // -1 indicates the header
|
||||
@ -230,10 +271,7 @@ private fun LongPressMenuContent(
|
||||
LongPressMenuButton(
|
||||
icon = action.type.icon,
|
||||
text = stringResource(action.type.label),
|
||||
onClick = {
|
||||
action.action(ctx)
|
||||
onDismissRequest()
|
||||
},
|
||||
onClick = { runActionAndDismiss(action) },
|
||||
enabled = action.enabled(false),
|
||||
modifier = Modifier
|
||||
.height(buttonHeight)
|
||||
@ -296,7 +334,12 @@ fun LongPressMenuDragHandle(onEditActions: () -> Unit) {
|
||||
// the focus to "nothing focused". Ideally it would be great to focus the first item in
|
||||
// the long press menu, but then there would need to be a way to ignore the UP from the
|
||||
// DPAD after an externally-triggered long press.
|
||||
Box(Modifier.size(1.dp).focusable().onFocusChanged { showFocusTrap = !it.isFocused })
|
||||
Box(
|
||||
Modifier
|
||||
.size(1.dp)
|
||||
.focusable()
|
||||
.onFocusChanged { showFocusTrap = !it.isFocused }
|
||||
)
|
||||
}
|
||||
BottomSheetDefaults.DragHandle(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
@ -693,7 +736,7 @@ private fun LongPressMenuPreview(
|
||||
actions = LongPressAction.Type.entries
|
||||
// disable Enqueue actions just to show it off
|
||||
.map { t -> t.buildAction({ t != EnqueueNext }) { } },
|
||||
onDismissRequest = {},
|
||||
runActionAndDismiss = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import org.schabi.newpipe.util.Either
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
@ -71,6 +72,19 @@ data class LongPressable(
|
||||
decoration = Decoration.from(item.streamType, item.duration),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun fromPlayQueueItem(item: PlayQueueItem) = LongPressable(
|
||||
title = item.title,
|
||||
url = item.url.takeIf { it.isNotBlank() },
|
||||
thumbnailUrl = ImageStrategy.choosePreferredImage(item.thumbnails),
|
||||
uploader = item.uploader.takeIf { it.isNotBlank() },
|
||||
uploaderUrl = item.uploaderUrl?.takeIf { it.isNotBlank() },
|
||||
viewCount = null,
|
||||
streamType = item.streamType,
|
||||
uploadDate = null,
|
||||
decoration = Decoration.from(item.streamType, item.duration),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun fromPlaylistMetadataEntry(item: PlaylistMetadataEntry) = LongPressable(
|
||||
// many fields are null because this is a local playlist
|
||||
@ -118,7 +132,7 @@ data class LongPressable(
|
||||
title = item.name,
|
||||
url = item.url?.takeIf { it.isNotBlank() },
|
||||
thumbnailUrl = ImageStrategy.choosePreferredImage(item.thumbnails),
|
||||
uploader = item.uploaderName.takeIf { it.isNotBlank() },
|
||||
uploader = item.uploaderName?.takeIf { it.isNotBlank() },
|
||||
uploaderUrl = item.uploaderUrl?.takeIf { it.isNotBlank() },
|
||||
viewCount = null,
|
||||
streamType = null,
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
package org.schabi.newpipe.ui.components.menu
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.utils.Utils
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
|
||||
// Utilities for fetching additional data for stream items when needed.
|
||||
|
||||
/**
|
||||
* Use this to certainly obtain an single play queue with all of the data filled in when the
|
||||
* stream info item you are handling might be sparse, e.g. because it was fetched via a
|
||||
* [org.schabi.newpipe.extractor.feed.FeedExtractor]. FeedExtractors provide a fast and
|
||||
* lightweight method to fetch info, but the info might be incomplete (see
|
||||
* [org.schabi.newpipe.local.feed.service.FeedLoadService] for more details).
|
||||
*
|
||||
* @param context Android context
|
||||
* @param item item which is checked and eventually loaded completely
|
||||
* @return a [SinglePlayQueue] with full data (fetched if necessary)
|
||||
*/
|
||||
@MainThread
|
||||
suspend fun fetchItemInfoIfSparse(
|
||||
context: Context,
|
||||
item: StreamInfoItem,
|
||||
): SinglePlayQueue {
|
||||
if ((StreamTypeUtil.isLiveStream(item.streamType) || item.duration >= 0) &&
|
||||
!Utils.isNullOrEmpty(item.uploaderUrl)
|
||||
) {
|
||||
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
|
||||
// uploader url, probably all info is already there, so there is no need to fetch it
|
||||
return SinglePlayQueue(item)
|
||||
}
|
||||
|
||||
// either the duration or the uploader url are not available, so fetch more info
|
||||
val streamInfo = fetchStreamInfoAndSaveToDatabase(context, item.serviceId, item.url)
|
||||
return SinglePlayQueue(streamInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to certainly obtain an uploader url when the stream info item or play queue item you
|
||||
* are handling might not have the uploader url (e.g. because it was fetched with
|
||||
* [org.schabi.newpipe.extractor.feed.FeedExtractor]). A toast is shown if loading details is
|
||||
* required.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param serviceId serviceId of the item
|
||||
* @param url item url
|
||||
* @param uploaderUrl uploaderUrl of the item; if null or empty will be fetched
|
||||
* @return the original or the fetched uploader URL (may still be null if the extractor didn't
|
||||
* provide one)
|
||||
*/
|
||||
@MainThread
|
||||
suspend fun fetchUploaderUrlIfSparse(
|
||||
context: Context,
|
||||
serviceId: Int,
|
||||
url: String,
|
||||
uploaderUrl: String?,
|
||||
): String? {
|
||||
if (!uploaderUrl.isNullOrEmpty()) {
|
||||
return uploaderUrl
|
||||
}
|
||||
val streamInfo = fetchStreamInfoAndSaveToDatabase(context, serviceId, url)
|
||||
return streamInfo.uploaderUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the stream info corresponding to the given data on an I/O thread, stores the result in
|
||||
* the database, and returns. A toast will be shown to the user about loading stream details, so
|
||||
* this needs to be called on the main thread.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param serviceId service id of the stream to load
|
||||
* @param url url of the stream to load
|
||||
* @return the fetched [StreamInfo]
|
||||
*/
|
||||
@MainThread
|
||||
suspend fun fetchStreamInfoAndSaveToDatabase(
|
||||
context: Context,
|
||||
serviceId: Int,
|
||||
url: String,
|
||||
): StreamInfo {
|
||||
Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val streamInfo = ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.await()
|
||||
// save to database
|
||||
NewPipeDatabase.getInstance(context)
|
||||
.streamDAO()
|
||||
.upsert(StreamEntity(streamInfo))
|
||||
return@withContext streamInfo
|
||||
}
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Utility class for fetching additional data for stream items when needed.
|
||||
*/
|
||||
public final class SparseItemUtil {
|
||||
private SparseItemUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to certainly obtain an single play queue with all of the data filled in when the
|
||||
* stream info item you are handling might be sparse, e.g. because it was fetched via a {@link
|
||||
* org.schabi.newpipe.extractor.feed.FeedExtractor}. FeedExtractors provide a fast and
|
||||
* lightweight method to fetch info, but the info might be incomplete (see
|
||||
* {@link org.schabi.newpipe.local.feed.service.FeedLoadService} for more details).
|
||||
*
|
||||
* @param context Android context
|
||||
* @param item item which is checked and eventually loaded completely
|
||||
* @param callback callback to call with the single play queue built from the original item if
|
||||
* all info was available, otherwise from the fetched {@link
|
||||
* org.schabi.newpipe.extractor.stream.StreamInfo}
|
||||
*/
|
||||
public static void fetchItemInfoIfSparse(@NonNull final Context context,
|
||||
@NonNull final StreamInfoItem item,
|
||||
@NonNull final Consumer<SinglePlayQueue> callback) {
|
||||
if ((StreamTypeUtil.isLiveStream(item.getStreamType()) || item.getDuration() >= 0)
|
||||
&& !isNullOrEmpty(item.getUploaderUrl())) {
|
||||
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
|
||||
// uploader url, probably all info is already there, so there is no need to fetch it
|
||||
callback.accept(new SinglePlayQueue(item));
|
||||
return;
|
||||
}
|
||||
|
||||
// either the duration or the uploader url are not available, so fetch more info
|
||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||
streamInfo -> callback.accept(new SinglePlayQueue(streamInfo)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to certainly obtain an uploader url when the stream info item or play queue item you
|
||||
* are handling might not have the uploader url (e.g. because it was fetched with {@link
|
||||
* org.schabi.newpipe.extractor.feed.FeedExtractor}). A toast is shown if loading details is
|
||||
* required.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param serviceId serviceId of the item
|
||||
* @param url item url
|
||||
* @param uploaderUrl uploaderUrl of the item; if null or empty will be fetched
|
||||
* @param callback callback to be called with either the original uploaderUrl, if it was a
|
||||
* valid url, otherwise with the uploader url obtained by fetching the {@link
|
||||
* org.schabi.newpipe.extractor.stream.StreamInfo} corresponding to the item
|
||||
*/
|
||||
public static void fetchUploaderUrlIfSparse(@NonNull final Context context,
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
@Nullable final String uploaderUrl,
|
||||
@NonNull final Consumer<String> callback) {
|
||||
if (!isNullOrEmpty(uploaderUrl)) {
|
||||
callback.accept(uploaderUrl);
|
||||
return;
|
||||
}
|
||||
fetchStreamInfoAndSaveToDatabase(context, serviceId, url,
|
||||
streamInfo -> callback.accept(streamInfo.getUploaderUrl()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the stream info corresponding to the given data on an I/O thread, stores the result in
|
||||
* the database and calls the callback on the main thread with the result. A toast will be shown
|
||||
* to the user about loading stream details, so this needs to be called on the main thread.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param serviceId service id of the stream to load
|
||||
* @param url url of the stream to load
|
||||
* @param callback callback to be called with the result
|
||||
*/
|
||||
public static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context context,
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
final Consumer<StreamInfo> callback) {
|
||||
Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show();
|
||||
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
// save to database in the background (not on main thread)
|
||||
Completable.fromAction(() -> NewPipeDatabase.getInstance(context)
|
||||
.streamDAO().upsert(new StreamEntity(result)))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.doOnError(throwable ->
|
||||
ErrorUtil.createNotification(context,
|
||||
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
"Saving stream info to database", result)))
|
||||
.subscribe();
|
||||
|
||||
// call callback on main thread with the obtained result
|
||||
callback.accept(result);
|
||||
}, throwable -> ErrorUtil.createNotification(context,
|
||||
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
"Loading stream info: " + url, serviceId, url)
|
||||
));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user