mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-10-04 02:00:51 +02:00
Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a95a5ed13e | ||
![]() |
da61c9f915 | ||
![]() |
9472c36cbd | ||
![]() |
49c12a31e9 | ||
![]() |
fc061599f8 | ||
![]() |
b066457ccf | ||
![]() |
2c5c7dfe3a | ||
![]() |
4573407fc7 | ||
![]() |
9912c11043 | ||
![]() |
231c5e515f | ||
![]() |
e9870d9e1d | ||
![]() |
c274ee9873 | ||
![]() |
c8caf48cda | ||
![]() |
1de662f779 | ||
![]() |
84887395f8 | ||
![]() |
bf766f1670 | ||
![]() |
51bdc30ed0 | ||
![]() |
4b892e2b30 | ||
![]() |
43b2176956 | ||
![]() |
00283fac30 | ||
![]() |
78f6a86645 | ||
![]() |
9d2ab61993 | ||
![]() |
8fdd828de4 | ||
![]() |
25795c3a96 | ||
![]() |
7f3da04fee | ||
![]() |
7864521cb4 | ||
![]() |
31b83ba47a | ||
![]() |
9524c6245d | ||
![]() |
57d2fe113a | ||
![]() |
2f6cb87bba | ||
![]() |
3cef7f3201 | ||
![]() |
2225933946 | ||
![]() |
47259ef152 | ||
![]() |
b2eb631a97 | ||
![]() |
9e0f37a2de | ||
![]() |
f712ea34e0 | ||
![]() |
a44b7c9c9e | ||
![]() |
4b32890b5f | ||
![]() |
a41aa01461 | ||
![]() |
2ed6819e2c | ||
![]() |
ea875c59af | ||
![]() |
a22162ffac | ||
![]() |
83d16dc656 | ||
![]() |
8ceefee1e3 | ||
![]() |
8f157be7e0 | ||
![]() |
38579e9a29 | ||
![]() |
30a91f59ae | ||
![]() |
0e169951f7 | ||
![]() |
8b9db369f6 | ||
![]() |
f7e10eb094 | ||
![]() |
0d73d193ad | ||
![]() |
40815086ad | ||
![]() |
16860603fd | ||
![]() |
c607089cbb | ||
![]() |
28464344c1 | ||
![]() |
ed68e3bd46 | ||
![]() |
082d7a3f18 | ||
![]() |
6eddaa0d38 | ||
![]() |
1aa1a0287e | ||
![]() |
3bfcb16f9a | ||
![]() |
f37d869ea2 | ||
![]() |
78547b4fa4 | ||
![]() |
29e56b9f2d | ||
![]() |
83357ca67e | ||
![]() |
8482bf9fed | ||
![]() |
2a98cca801 | ||
![]() |
6277d4981c | ||
![]() |
02deaa0f1a | ||
![]() |
4a278ef102 | ||
![]() |
7ab8f9f112 | ||
![]() |
7fca0e0786 | ||
![]() |
0b0dfd0a37 | ||
![]() |
dd07bd91a4 | ||
![]() |
ed4eb124e4 | ||
![]() |
4070007c93 | ||
![]() |
5b213a19e4 | ||
![]() |
34d81d3bf2 | ||
![]() |
8bc8355b68 | ||
![]() |
ab99c14fd2 | ||
![]() |
1047158a66 | ||
![]() |
fe227d5b94 | ||
![]() |
cb80891a5f | ||
![]() |
9db0133a5b | ||
![]() |
464a646671 | ||
![]() |
408a71cfdc | ||
![]() |
6399e39507 | ||
![]() |
f9443f7421 | ||
![]() |
697b8411df | ||
![]() |
e136a6f915 | ||
![]() |
8dce66d76f |
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
@@ -22,6 +22,7 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
|
||||
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
|
||||
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
* NewPipe uses the [PrettyTime](https://github.com/ocpsoft/prettytime) library to display localized versions of dates and times. It needs to be translated, too. Read [these instructions to add a new language](https://www.ocpsoft.org/prettytime/#section-14) and [this issue](https://github.com/TeamNewPipe/NewPipe/issues/9134) for more info.
|
||||
|
||||
## Code contribution
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: [enhancement, needs triage]
|
||||
labels: [feature request, needs triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@@ -8,16 +8,16 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
buildToolsVersion '31.0.0'
|
||||
compileSdk 32
|
||||
namespace 'org.schabi.newpipe'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 29
|
||||
versionCode 990
|
||||
versionName "0.24.0"
|
||||
versionCode 991
|
||||
versionName "0.24.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -187,7 +187,7 @@ dependencies {
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c710da160f488bb40ab2cf4469bec9bd4cefd38'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:eb07d70a2ce03bee3cc74fc33b2e4173e1c21436'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
@@ -198,7 +198,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
@@ -271,7 +271,7 @@ dependencies {
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final"
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
|
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".DebugApp"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -14,6 +13,9 @@
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -151,6 +153,7 @@
|
||||
<data android:pathPrefix="/channel/" />
|
||||
<data android:pathPrefix="/user/" />
|
||||
<data android:pathPrefix="/c/" />
|
||||
<data android:pathPrefix="/@" />
|
||||
<!-- playlist prefix -->
|
||||
<data android:pathPrefix="/playlist" />
|
||||
</intent-filter>
|
||||
|
@@ -78,6 +78,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
aboutDonationLink.openLink(R.string.donation_url)
|
||||
aboutWebsiteLink.openLink(R.string.website_url)
|
||||
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||
faqLink.openLink(R.string.faq_url)
|
||||
return root
|
||||
}
|
||||
}
|
||||
|
@@ -48,7 +48,10 @@ abstract class FeedDAO {
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = f.subscription_id
|
||||
ON (
|
||||
:groupId <> ${FeedGroupEntity.GROUP_ALL_ID}
|
||||
AND fgs.subscription_id = f.subscription_id
|
||||
)
|
||||
|
||||
WHERE (
|
||||
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}
|
||||
|
@@ -145,6 +145,12 @@ public class DownloadDialog extends DialogFragment
|
||||
// Instance creation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public DownloadDialog() {
|
||||
// Just an empty default no-arg ctor to keep Fragment.instantiate() happy
|
||||
// otherwise InstantiationException will be thrown when fragment is recreated
|
||||
// TODO: Maybe use a custom FragmentFactory instead?
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new download dialog with the video, audio and subtitle streams from the provided
|
||||
* stream info. Video streams and video-only streams will be put into a single list menu,
|
||||
@@ -153,7 +159,7 @@ public class DownloadDialog extends DialogFragment
|
||||
* @param context the context to use just to obtain preferences and strings (will not be stored)
|
||||
* @param info the info from which to obtain downloadable streams and other info (e.g. title)
|
||||
*/
|
||||
public DownloadDialog(final Context context, @NonNull final StreamInfo info) {
|
||||
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
|
||||
this.currentInfo = info;
|
||||
|
||||
// TODO: Adapt this code when the downloader support other types of stream deliveries
|
||||
|
@@ -30,6 +30,7 @@ import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.isInterruptedCaused
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ErrorPanelHelper(
|
||||
@@ -52,6 +53,8 @@ class ErrorPanelHelper(
|
||||
errorPanelRoot.findViewById(R.id.error_action_button)
|
||||
private val errorRetryButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_retry_button)
|
||||
private val errorOpenInBrowserButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_open_in_browser)
|
||||
|
||||
private var errorDisposable: Disposable? = null
|
||||
|
||||
@@ -69,6 +72,7 @@ class ErrorPanelHelper(
|
||||
errorServiceExplanationTextView.isVisible = false
|
||||
errorActionButton.isVisible = false
|
||||
errorRetryButton.isVisible = false
|
||||
errorOpenInBrowserButton.isVisible = false
|
||||
}
|
||||
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
@@ -99,6 +103,7 @@ class ErrorPanelHelper(
|
||||
}
|
||||
|
||||
errorRetryButton.isVisible = true
|
||||
showAndSetOpenInBrowserButtonAction(errorInfo)
|
||||
} else if (errorInfo.throwable is AccountTerminatedException) {
|
||||
errorTextView.setText(R.string.account_terminated)
|
||||
|
||||
@@ -128,6 +133,7 @@ class ErrorPanelHelper(
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorRetryButton.isVisible = true
|
||||
}
|
||||
showAndSetOpenInBrowserButtonAction(errorInfo)
|
||||
}
|
||||
|
||||
setRootVisible()
|
||||
@@ -145,6 +151,15 @@ class ErrorPanelHelper(
|
||||
errorActionButton.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun showAndSetOpenInBrowserButtonAction(
|
||||
errorInfo: ErrorInfo
|
||||
) {
|
||||
errorOpenInBrowserButton.isVisible = true
|
||||
errorOpenInBrowserButton.setOnClickListener {
|
||||
ShareUtils.openUrlInBrowser(context, errorInfo.request, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
ensureDefaultVisibility()
|
||||
|
||||
|
@@ -248,6 +248,7 @@ public final class VideoDetailFragment
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -337,6 +338,8 @@ public final class VideoDetailFragment
|
||||
|
||||
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
||||
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
|
||||
setupBrightness();
|
||||
|
||||
if (tabSettingsChanged) {
|
||||
@@ -526,6 +529,9 @@ public final class VideoDetailFragment
|
||||
case R.id.overlay_buttons_layout:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
break;
|
||||
case R.id.overlay_play_queue_button:
|
||||
NavigationHelper.openPlayQueue(getContext());
|
||||
break;
|
||||
case R.id.overlay_play_pause_button:
|
||||
if (playerIsNotStopped()) {
|
||||
player.playPause();
|
||||
@@ -684,6 +690,7 @@ public final class VideoDetailFragment
|
||||
binding.overlayMetadataLayout.setOnClickListener(this);
|
||||
binding.overlayMetadataLayout.setOnLongClickListener(this);
|
||||
binding.overlayButtonsLayout.setOnClickListener(this);
|
||||
binding.overlayPlayQueueButton.setOnClickListener(this);
|
||||
binding.overlayCloseButton.setOnClickListener(this);
|
||||
binding.overlayPlayPauseButton.setOnClickListener(this);
|
||||
|
||||
@@ -1816,6 +1823,14 @@ public final class VideoDetailFragment
|
||||
+ title + "], playQueue = [" + playQueue + "]");
|
||||
}
|
||||
|
||||
// Register broadcast receiver to listen to playQueue changes
|
||||
// and hide the overlayPlayQueueButton when the playQueue is empty / destroyed.
|
||||
if (playQueue != null && playQueue.getBroadcastReceiver() != null) {
|
||||
playQueue.getBroadcastReceiver().subscribe(
|
||||
event -> updateOverlayPlayQueueButtonVisibility()
|
||||
);
|
||||
}
|
||||
|
||||
// This should be the only place where we push data to stack.
|
||||
// It will allow to have live instance of PlayQueue with actual information about
|
||||
// deleted/added items inside Channel/Playlist queue and makes possible to have
|
||||
@@ -1922,6 +1937,7 @@ public final class VideoDetailFragment
|
||||
currentInfo.getUploaderName(),
|
||||
currentInfo.getThumbnailUrl());
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2388,6 +2404,18 @@ public final class VideoDetailFragment
|
||||
});
|
||||
}
|
||||
|
||||
private void updateOverlayPlayQueueButtonVisibility() {
|
||||
final boolean isPlayQueueEmpty =
|
||||
player == null // no player => no play queue :)
|
||||
|| player.getPlayQueue() == null
|
||||
|| player.getPlayQueue().isEmpty();
|
||||
if (binding != null) {
|
||||
// binding is null when rotating the device...
|
||||
binding.overlayPlayQueueButton.setVisibility(
|
||||
isPlayQueueEmpty ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOverlayData(@Nullable final String overlayTitle,
|
||||
@Nullable final String uploader,
|
||||
@Nullable final String thumbnailUrl) {
|
||||
@@ -2426,6 +2454,7 @@ public final class VideoDetailFragment
|
||||
binding.overlayMetadataLayout.setClickable(enable);
|
||||
binding.overlayMetadataLayout.setLongClickable(enable);
|
||||
binding.overlayButtonsLayout.setClickable(enable);
|
||||
binding.overlayPlayQueueButton.setClickable(enable);
|
||||
binding.overlayPlayPauseButton.setClickable(enable);
|
||||
binding.overlayCloseButton.setClickable(enable);
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingSc
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -31,6 +30,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.SuperScrollLayoutManager;
|
||||
|
||||
import java.util.List;
|
||||
@@ -476,15 +476,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
|
||||
protected boolean isGridLayout() {
|
||||
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.list_view_mode_key),
|
||||
getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(listMode)) {
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||
} else {
|
||||
return "grid".equals(listMode);
|
||||
}
|
||||
return ThemeHelper.shouldUseGridLayout(activity);
|
||||
}
|
||||
}
|
||||
|
@@ -61,5 +61,6 @@ class StreamSegmentAdapter(
|
||||
|
||||
interface StreamSegmentListener {
|
||||
fun onItemClick(item: StreamSegmentItem, seconds: Int)
|
||||
fun onItemLongClick(item: StreamSegmentItem, seconds: Int)
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,7 @@ class StreamSegmentItem(
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
||||
viewHolder.root.isSelected = isSelected
|
||||
}
|
||||
|
||||
|
@@ -112,12 +112,19 @@ public enum StreamDialogDefaultEntry {
|
||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||
item.getThumbnailUrl())),
|
||||
|
||||
/**
|
||||
* Opens a {@link DownloadDialog} after fetching some stream info.
|
||||
* If the user quits the current fragment, it will not open a DownloadDialog.
|
||||
*/
|
||||
DOWNLOAD(R.string.download, (fragment, item) ->
|
||||
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
|
||||
item.getUrl(), info -> {
|
||||
final DownloadDialog downloadDialog =
|
||||
new DownloadDialog(fragment.requireContext(), info);
|
||||
downloadDialog.show(fragment.getChildFragmentManager(), "downloadDialog");
|
||||
if (fragment.getContext() != null) {
|
||||
final DownloadDialog downloadDialog =
|
||||
new DownloadDialog(fragment.requireContext(), info);
|
||||
downloadDialog.show(fragment.getChildFragmentManager(),
|
||||
"downloadDialog");
|
||||
}
|
||||
})
|
||||
),
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
@@ -27,7 +28,7 @@ import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private static final String TAG = "CommentsMiniIIHolder";
|
||||
@@ -39,7 +40,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private final RelativeLayout itemRoot;
|
||||
public final ImageView itemThumbnailView;
|
||||
private final ImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemPublishedTime;
|
||||
@@ -47,27 +48,6 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private String commentText;
|
||||
private String streamUrl;
|
||||
|
||||
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(final Matcher match, final String url) {
|
||||
try {
|
||||
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
|
||||
TimestampExtractor.getTimestampFromMatcher(match, commentText);
|
||||
|
||||
if (timestampMatchDTO == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return streamUrl + url.replace(
|
||||
match.group(0),
|
||||
"#timestamp=" + timestampMatchDTO.seconds());
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
|
||||
final ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
@@ -115,7 +95,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
itemContentView.setLines(COMMENT_DEFAULT_LINES);
|
||||
commentText = item.getCommentText();
|
||||
itemContentView.setText(commentText);
|
||||
itemContentView.setText(commentText, TextView.BufferType.SPANNABLE);
|
||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||
|
||||
if (itemContentView.getLineCount() == 0) {
|
||||
@@ -243,14 +223,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
private void linkify() {
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
Linkify.WEB_URLS);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
TimestampExtractor.TIMESTAMPS_PATTERN,
|
||||
null,
|
||||
null,
|
||||
timestampLink);
|
||||
LinkifyCompat.addLinks(itemContentView, Linkify.WEB_URLS);
|
||||
LinkifyCompat.addLinks(itemContentView, TimestampExtractor.TIMESTAMPS_PATTERN, null, null,
|
||||
(match, url) -> {
|
||||
try {
|
||||
final var timestampMatch = TimestampExtractor
|
||||
.getTimestampFromMatcher(match, commentText);
|
||||
if (timestampMatch == null) {
|
||||
return url;
|
||||
}
|
||||
return streamUrl + url.replace(Objects.requireNonNull(match.group(0)),
|
||||
"#timestamp=" + timestampMatch.seconds());
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
|
||||
return url;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -40,6 +40,7 @@ import androidx.annotation.Nullable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.MenuItemCompat
|
||||
import androidx.core.view.isVisible
|
||||
@@ -603,7 +604,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
// state until the user scrolls them out of the visible area which causes a update/bind-call
|
||||
groupAdapter.notifyItemRangeChanged(
|
||||
0,
|
||||
minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount))
|
||||
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount)
|
||||
)
|
||||
|
||||
if (highlightCount > 0) {
|
||||
|
@@ -11,6 +11,7 @@ import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -22,6 +23,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
@@ -34,7 +36,6 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
@@ -55,7 +56,6 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -63,7 +63,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -309,7 +308,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
|
||||
return new Subscriber<List<PlaylistStreamEntry>>() {
|
||||
return new Subscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(final Subscription s) {
|
||||
showLoading();
|
||||
@@ -395,31 +394,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
isRemovingWatched = true;
|
||||
showLoading();
|
||||
|
||||
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map((List<PlaylistStreamEntry> playlist) -> {
|
||||
// Playlist data
|
||||
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
|
||||
|
||||
// History data
|
||||
final HistoryRecordManager recordManager =
|
||||
new HistoryRecordManager(getContext());
|
||||
final Iterator<StreamHistoryEntry> historyIter = recordManager
|
||||
.getStreamHistorySortedById().blockingFirst().iterator();
|
||||
|
||||
final var recordManager = new HistoryRecordManager(getContext());
|
||||
final var historyIdsMaybe = recordManager.getStreamHistorySortedById()
|
||||
.firstElement()
|
||||
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
|
||||
.map(historyList -> historyList.stream().map(StreamHistoryEntry::getStreamId)
|
||||
.collect(Collectors.toList()));
|
||||
final var streamsMaybe = playlistManager.getPlaylistStreams(playlistId)
|
||||
.firstElement()
|
||||
.zipWith(historyIdsMaybe, (playlist, historyStreamIds) -> {
|
||||
// Remove Watched, Functionality data
|
||||
final List<PlaylistStreamEntry> notWatchedItems = new ArrayList<>();
|
||||
boolean thumbnailVideoRemoved = false;
|
||||
|
||||
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
|
||||
final ArrayList<Long> historyStreamIds = new ArrayList<>();
|
||||
while (historyIter.hasNext()) {
|
||||
historyStreamIds.add(historyIter.next().getStreamId());
|
||||
}
|
||||
|
||||
if (removePartiallyWatched) {
|
||||
while (playlistIter.hasNext()) {
|
||||
final PlaylistStreamEntry playlistItem = playlistIter.next();
|
||||
for (final var playlistItem : playlist) {
|
||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||
playlistItem.getStreamId());
|
||||
|
||||
@@ -432,14 +421,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final Iterator<StreamStateEntity> streamStatesIter = recordManager
|
||||
.loadLocalStreamStateBatch(playlist).blockingGet().iterator();
|
||||
final var streamStates = recordManager
|
||||
.loadLocalStreamStateBatch(playlist).blockingGet();
|
||||
|
||||
for (int i = 0; i < playlist.size(); i++) {
|
||||
final var playlistItem = playlist.get(i);
|
||||
final var streamStateEntity = streamStates.get(i);
|
||||
|
||||
while (playlistIter.hasNext()) {
|
||||
final PlaylistStreamEntry playlistItem = playlistIter.next();
|
||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||
playlistItem.getStreamId());
|
||||
final StreamStateEntity streamStateEntity = streamStatesIter.next();
|
||||
final long duration = playlistItem.toStreamInfoItem().getDuration();
|
||||
|
||||
if (indexInHistory < 0 || (streamStateEntity != null
|
||||
@@ -453,19 +443,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
|
||||
return Flowable.just(notWatchedItems, thumbnailVideoRemoved);
|
||||
})
|
||||
return new Pair<>(notWatchedItems, thumbnailVideoRemoved);
|
||||
});
|
||||
|
||||
disposables.add(streamsMaybe.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(flow -> {
|
||||
final List<PlaylistStreamEntry> notWatchedItems =
|
||||
(List<PlaylistStreamEntry>) flow.blockingFirst();
|
||||
final boolean thumbnailVideoRemoved = (Boolean) flow.blockingLast();
|
||||
final List<PlaylistStreamEntry> notWatchedItems = flow.first;
|
||||
final boolean thumbnailVideoRemoved = flow.second;
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
itemListAdapter.addItems(notWatchedItems);
|
||||
saveChanges();
|
||||
|
||||
|
||||
if (thumbnailVideoRemoved) {
|
||||
updateThumbnailUrl();
|
||||
}
|
||||
@@ -503,13 +493,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
||||
showHoldToAppendTipIfNeeded();
|
||||
});
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false);
|
||||
showHoldToAppendTipIfNeeded();
|
||||
});
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false);
|
||||
showHoldToAppendTipIfNeeded();
|
||||
});
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
@@ -523,6 +518,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
private void showHoldToAppendTipIfNeeded() {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
||||
Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -5,25 +5,45 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.xwray.groupie.Group
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.item.ChannelItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
|
||||
private var subscriptionManager = SubscriptionManager(application)
|
||||
|
||||
private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
|
||||
private val mutableFeedGroupsLiveData = MutableLiveData<List<Group>>()
|
||||
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
|
||||
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
|
||||
// true -> list view, false -> grid view
|
||||
private val listViewMode = BehaviorProcessor.createDefault(
|
||||
!ThemeHelper.shouldUseGridLayout(application)
|
||||
)
|
||||
private val listViewModeFlowable = listViewMode.distinctUntilChanged()
|
||||
|
||||
private var feedGroupItemsDisposable = feedDatabaseManager.groups()
|
||||
private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
|
||||
private val mutableFeedGroupsLiveData = MutableLiveData<Pair<List<Group>, Boolean>>()
|
||||
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
|
||||
val feedGroupsLiveData: LiveData<Pair<List<Group>, Boolean>> = mutableFeedGroupsLiveData
|
||||
|
||||
private var feedGroupItemsDisposable = Flowable
|
||||
.combineLatest(
|
||||
feedDatabaseManager.groups(),
|
||||
listViewModeFlowable,
|
||||
::Pair
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map(::FeedGroupCardItem) }
|
||||
.map { (feedGroups, listViewMode) ->
|
||||
Pair(
|
||||
feedGroups.map(if (listViewMode) ::FeedGroupCardItem else ::FeedGroupCardGridItem),
|
||||
listViewMode
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableFeedGroupsLiveData.postValue(it) },
|
||||
@@ -45,6 +65,14 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
|
||||
feedGroupItemsDisposable.dispose()
|
||||
}
|
||||
|
||||
fun setListViewMode(newListViewMode: Boolean) {
|
||||
listViewMode.onNext(newListViewMode)
|
||||
}
|
||||
|
||||
fun getListViewMode(): Boolean {
|
||||
return listViewMode.value ?: true
|
||||
}
|
||||
|
||||
sealed class SubscriptionState {
|
||||
data class LoadedState(val subscriptions: List<Group>) : SubscriptionState()
|
||||
data class ErrorState(val error: Throwable? = null) : SubscriptionState()
|
||||
|
@@ -1,35 +0,0 @@
|
||||
package org.schabi.newpipe.local.subscription.decoration
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.schabi.newpipe.R
|
||||
|
||||
class FeedGroupCarouselDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val marginStartEnd: Int
|
||||
private val marginTopBottom: Int
|
||||
private val marginBetweenItems: Int
|
||||
|
||||
init {
|
||||
with(context.resources) {
|
||||
marginStartEnd = getDimensionPixelOffset(R.dimen.feed_group_carousel_start_end_margin)
|
||||
marginTopBottom = getDimensionPixelOffset(R.dimen.feed_group_carousel_top_bottom_margin)
|
||||
marginBetweenItems = getDimensionPixelOffset(R.dimen.feed_group_carousel_between_items_margin)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, child: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val childAdapterPosition = parent.getChildAdapterPosition(child)
|
||||
val childAdapterCount = parent.adapter?.itemCount ?: 0
|
||||
|
||||
outRect.set(marginBetweenItems, marginTopBottom, 0, marginTopBottom)
|
||||
|
||||
if (childAdapterPosition == 0) {
|
||||
outRect.left = marginStartEnd
|
||||
} else if (childAdapterPosition == childAdapterCount - 1) {
|
||||
outRect.right = marginStartEnd
|
||||
}
|
||||
}
|
||||
}
|
@@ -124,11 +124,13 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
FeedGroupDialogViewModel.Factory(
|
||||
FeedGroupDialogViewModel.getFactory(
|
||||
requireContext(),
|
||||
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped
|
||||
groupId,
|
||||
subscriptionsCurrentSearchQuery,
|
||||
subscriptionsShowOnlyUngrouped
|
||||
)
|
||||
).get(FeedGroupDialogViewModel::class.java)
|
||||
)[FeedGroupDialogViewModel::class.java]
|
||||
|
||||
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
||||
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
|
||||
|
@@ -4,7 +4,8 @@ import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
@@ -115,18 +116,18 @@ class FeedGroupDialogViewModel(
|
||||
|
||||
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
private val initialQuery: String = "",
|
||||
private val initialShowOnlyUngrouped: Boolean = false
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return FeedGroupDialogViewModel(
|
||||
context.applicationContext,
|
||||
groupId, initialQuery, initialShowOnlyUngrouped
|
||||
) as T
|
||||
companion object {
|
||||
fun getFactory(
|
||||
context: Context,
|
||||
groupId: Long,
|
||||
initialQuery: String,
|
||||
initialShowOnlyUngrouped: Boolean
|
||||
) = viewModelFactory {
|
||||
initializer {
|
||||
FeedGroupDialogViewModel(
|
||||
context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedGroupAddNewGridItemBinding
|
||||
|
||||
class FeedGroupAddNewGridItem : BindableItem<FeedGroupAddNewGridItemBinding>() {
|
||||
override fun getLayout(): Int = R.layout.feed_group_add_new_grid_item
|
||||
override fun initializeViewBinding(view: View) = FeedGroupAddNewGridItemBinding.bind(view)
|
||||
override fun bind(viewBinding: FeedGroupAddNewGridItemBinding, position: Int) {
|
||||
// this is a static item, nothing to do here
|
||||
}
|
||||
}
|
@@ -5,8 +5,10 @@ import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding
|
||||
|
||||
class FeedGroupAddItem : BindableItem<FeedGroupAddNewItemBinding>() {
|
||||
class FeedGroupAddNewItem : BindableItem<FeedGroupAddNewItemBinding>() {
|
||||
override fun getLayout(): Int = R.layout.feed_group_add_new_item
|
||||
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {}
|
||||
override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view)
|
||||
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {
|
||||
// this is a static item, nothing to do here
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.FeedGroupCardGridItemBinding
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
data class FeedGroupCardGridItem(
|
||||
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
val name: String,
|
||||
val icon: FeedGroupIcon,
|
||||
) : BindableItem<FeedGroupCardGridItemBinding>() {
|
||||
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
|
||||
|
||||
override fun getId(): Long {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> super.getId()
|
||||
else -> groupId
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayout(): Int = R.layout.feed_group_card_grid_item
|
||||
|
||||
override fun bind(viewBinding: FeedGroupCardGridItemBinding, position: Int) {
|
||||
viewBinding.title.text = name
|
||||
viewBinding.icon.setImageResource(icon.getDrawableRes())
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = FeedGroupCardGridItemBinding.bind(view)
|
||||
}
|
@@ -1,60 +1,82 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
|
||||
import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
|
||||
|
||||
class FeedGroupCarouselItem(
|
||||
context: Context,
|
||||
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
|
||||
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>,
|
||||
var listViewMode: Boolean
|
||||
) : BindableItem<FeedItemCarouselBinding>() {
|
||||
private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context)
|
||||
companion object {
|
||||
const val PAYLOAD_UPDATE_LIST_VIEW_MODE = 2
|
||||
}
|
||||
|
||||
private var linearLayoutManager: LinearLayoutManager? = null
|
||||
private var carouselLayoutManager: LinearLayoutManager? = null
|
||||
private var listState: Parcelable? = null
|
||||
|
||||
override fun getLayout() = R.layout.feed_item_carousel
|
||||
|
||||
fun onSaveInstanceState(): Parcelable? {
|
||||
listState = linearLayoutManager?.onSaveInstanceState()
|
||||
listState = carouselLayoutManager?.onSaveInstanceState()
|
||||
return listState
|
||||
}
|
||||
|
||||
fun onRestoreInstanceState(state: Parcelable?) {
|
||||
linearLayoutManager?.onRestoreInstanceState(state)
|
||||
carouselLayoutManager?.onRestoreInstanceState(state)
|
||||
listState = state
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View): FeedItemCarouselBinding {
|
||||
val viewHolder = FeedItemCarouselBinding.bind(view)
|
||||
val viewBinding = FeedItemCarouselBinding.bind(view)
|
||||
updateViewMode(viewBinding)
|
||||
return viewBinding
|
||||
}
|
||||
|
||||
linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
|
||||
|
||||
viewHolder.recyclerView.apply {
|
||||
layoutManager = linearLayoutManager
|
||||
adapter = carouselAdapter
|
||||
addItemDecoration(feedGroupCarouselDecoration)
|
||||
override fun bind(
|
||||
viewBinding: FeedItemCarouselBinding,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.contains(PAYLOAD_UPDATE_LIST_VIEW_MODE)) {
|
||||
updateViewMode(viewBinding)
|
||||
return
|
||||
}
|
||||
|
||||
return viewHolder
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) {
|
||||
viewBinding.recyclerView.apply { adapter = carouselAdapter }
|
||||
linearLayoutManager?.onRestoreInstanceState(listState)
|
||||
carouselLayoutManager?.onRestoreInstanceState(listState)
|
||||
}
|
||||
|
||||
override fun unbind(viewHolder: GroupieViewHolder<FeedItemCarouselBinding>) {
|
||||
super.unbind(viewHolder)
|
||||
listState = carouselLayoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
listState = linearLayoutManager?.onSaveInstanceState()
|
||||
private fun updateViewMode(viewBinding: FeedItemCarouselBinding) {
|
||||
viewBinding.recyclerView.apply { adapter = carouselAdapter }
|
||||
|
||||
val context = viewBinding.root.context
|
||||
carouselLayoutManager = if (listViewMode) {
|
||||
LinearLayoutManager(context)
|
||||
} else {
|
||||
GridLayoutManager(context, getGridSpanCount(context, DeviceUtils.dpToPx(112, context)))
|
||||
}
|
||||
|
||||
viewBinding.recyclerView.apply {
|
||||
layoutManager = carouselLayoutManager
|
||||
adapter = carouselAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,50 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.SubscriptionGroupsHeaderBinding
|
||||
|
||||
class GroupsHeader(
|
||||
private val title: String,
|
||||
private val onSortClicked: () -> Unit,
|
||||
private val onToggleListViewModeClicked: () -> Unit,
|
||||
var showSortButton: Boolean = true,
|
||||
var listViewMode: Boolean = true
|
||||
) : BindableItem<SubscriptionGroupsHeaderBinding>() {
|
||||
companion object {
|
||||
const val PAYLOAD_UPDATE_ICONS = 1
|
||||
}
|
||||
|
||||
override fun getLayout(): Int = R.layout.subscription_groups_header
|
||||
|
||||
override fun bind(
|
||||
viewBinding: SubscriptionGroupsHeaderBinding,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.contains(PAYLOAD_UPDATE_ICONS)) {
|
||||
updateIcons(viewBinding)
|
||||
return
|
||||
}
|
||||
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun bind(viewBinding: SubscriptionGroupsHeaderBinding, position: Int) {
|
||||
viewBinding.headerTitle.text = title
|
||||
viewBinding.headerSort.setOnClickListener { onSortClicked() }
|
||||
viewBinding.headerToggleViewMode.setOnClickListener { onToggleListViewModeClicked() }
|
||||
updateIcons(viewBinding)
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = SubscriptionGroupsHeaderBinding.bind(view)
|
||||
|
||||
private fun updateIcons(viewBinding: SubscriptionGroupsHeaderBinding) {
|
||||
viewBinding.headerToggleViewMode.setImageResource(
|
||||
if (listViewMode) R.drawable.ic_apps else R.drawable.ic_list
|
||||
)
|
||||
viewBinding.headerSort.isVisible = showSortButton
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.SubscriptionHeaderBinding
|
||||
|
||||
class Header(private val title: String) : BindableItem<SubscriptionHeaderBinding>() {
|
||||
|
||||
override fun getLayout(): Int = R.layout.subscription_header
|
||||
|
||||
override fun bind(viewBinding: SubscriptionHeaderBinding, position: Int) {
|
||||
viewBinding.root.text = title
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = SubscriptionHeaderBinding.bind(view)
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.HeaderWithMenuItemBinding
|
||||
|
||||
class HeaderWithMenuItem(
|
||||
val title: String,
|
||||
@DrawableRes val itemIcon: Int = 0,
|
||||
var showMenuItem: Boolean = true,
|
||||
private val onClickListener: (() -> Unit)? = null,
|
||||
private val menuItemOnClickListener: (() -> Unit)? = null
|
||||
) : BindableItem<HeaderWithMenuItemBinding>() {
|
||||
companion object {
|
||||
const val PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM = 1
|
||||
}
|
||||
|
||||
override fun getLayout(): Int = R.layout.header_with_menu_item
|
||||
|
||||
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.contains(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM)) {
|
||||
updateMenuItemVisibility(viewBinding)
|
||||
return
|
||||
}
|
||||
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int) {
|
||||
viewBinding.headerTitle.text = title
|
||||
viewBinding.headerMenuItem.setImageResource(itemIcon)
|
||||
|
||||
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
viewBinding.root.setOnClickListener(listener)
|
||||
|
||||
val menuItemListener = menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
|
||||
viewBinding.headerMenuItem.setOnClickListener(menuItemListener)
|
||||
updateMenuItemVisibility(viewBinding)
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = HeaderWithMenuItemBinding.bind(view)
|
||||
|
||||
private fun updateMenuItemVisibility(viewBinding: HeaderWithMenuItemBinding) {
|
||||
viewBinding.headerMenuItem.isVisible = showMenuItem
|
||||
}
|
||||
}
|
@@ -212,7 +212,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
|
||||
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||
unbind();
|
||||
finish();
|
||||
} else {
|
||||
onQueueUpdate(player.getPlayQueue());
|
||||
buildComponents();
|
||||
|
@@ -7,6 +7,7 @@ import android.view.View.OnTouchListener
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.isVisible
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
@@ -18,8 +19,6 @@ import org.schabi.newpipe.player.helper.PlayerHelper
|
||||
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* GestureListener for the player
|
||||
@@ -114,7 +113,7 @@ class MainPlayerGestureListener(
|
||||
|
||||
// Update progress bar
|
||||
val oldBrightness = layoutParams.screenBrightness
|
||||
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
|
||||
bar.progress = (bar.max * MathUtils.clamp(oldBrightness, 0f, 1f)).toInt()
|
||||
bar.incrementProgressBy(distanceY.toInt())
|
||||
|
||||
// Update brightness
|
||||
|
@@ -3,6 +3,7 @@ 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;
|
||||
import static org.schabi.newpipe.player.Player.STATE_PAUSED;
|
||||
@@ -52,6 +53,7 @@ import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
||||
import org.schabi.newpipe.info_list.StreamSegmentItem;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
@@ -60,6 +62,7 @@ import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
|
||||
import org.schabi.newpipe.player.gesture.MainPlayerGestureListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
@@ -69,6 +72,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -644,7 +648,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
private void buildSegments() {
|
||||
binding.itemsList.setAdapter(segmentAdapter);
|
||||
binding.itemsList.setClickable(true);
|
||||
binding.itemsList.setLongClickable(false);
|
||||
binding.itemsList.setLongClickable(true);
|
||||
|
||||
binding.itemsList.clearOnScrollListeners();
|
||||
if (itemTouchHelper != null) {
|
||||
@@ -696,10 +700,30 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
}
|
||||
|
||||
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
|
||||
return (item, seconds) -> {
|
||||
segmentAdapter.selectSegment(item);
|
||||
player.seekTo(seconds * 1000L);
|
||||
player.triggerProgressUpdate();
|
||||
return new StreamSegmentAdapter.StreamSegmentListener() {
|
||||
@Override
|
||||
public void onItemClick(@NonNull final StreamSegmentItem item, final int seconds) {
|
||||
segmentAdapter.selectSegment(item);
|
||||
player.seekTo(seconds * 1000L);
|
||||
player.triggerProgressUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(@NonNull final StreamSegmentItem item, final int seconds) {
|
||||
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
||||
if (currentMetadata == null
|
||||
|| currentMetadata.getServiceId() != YouTube.getServiceId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PlayQueueItem currentItem = player.getCurrentItem();
|
||||
if (currentItem != null) {
|
||||
String videoUrl = player.getVideoUrl();
|
||||
videoUrl += ("&t=" + seconds);
|
||||
ShareUtils.shareText(context, currentItem.getTitle(),
|
||||
videoUrl, currentItem.getThumbnailUrl());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user