mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-09-20 11:20:52 +02:00
Compare commits
159 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4e633504a8 | ||
![]() |
144a10f7a6 | ||
![]() |
72a2644f25 | ||
![]() |
ff8868f6a3 | ||
![]() |
8c6e37d1d1 | ||
![]() |
c90237c14c | ||
![]() |
3750561b4d | ||
![]() |
6b026557d4 | ||
![]() |
1ee137bbda | ||
![]() |
2c88e9d068 | ||
![]() |
4825a0a35f | ||
![]() |
122b0b0de4 | ||
![]() |
7dc85af5fb | ||
![]() |
c7daf32904 | ||
![]() |
4c8dca5300 | ||
![]() |
ef91214085 | ||
![]() |
dc09a4621b | ||
![]() |
2f99a217c3 | ||
![]() |
6992b2c308 | ||
![]() |
0d51eefbb9 | ||
![]() |
aa28a85747 | ||
![]() |
f18ee8e83d | ||
![]() |
fb58967766 | ||
![]() |
c3f1478fde | ||
![]() |
e5c00a7ef4 | ||
![]() |
769791af7a | ||
![]() |
e632fab4d0 | ||
![]() |
6cd25d7e55 | ||
![]() |
c9488eb042 | ||
![]() |
c8516a04dc | ||
![]() |
02d1b98b1c | ||
![]() |
d8236bbedd | ||
![]() |
1de21fb0c2 | ||
![]() |
13cac07b8d | ||
![]() |
bd9dcfb28a | ||
![]() |
d5199eac3e | ||
![]() |
7638d229c0 | ||
![]() |
a641c5bb58 | ||
![]() |
1e0c9f46ad | ||
![]() |
4eb02f584e | ||
![]() |
700c1b4b25 | ||
![]() |
4b4337e078 | ||
![]() |
38ce800685 | ||
![]() |
2310e8c1d6 | ||
![]() |
1b2b3a4f88 | ||
![]() |
d11129a76b | ||
![]() |
02789122a0 | ||
![]() |
676bc02d52 | ||
![]() |
8b807b0706 | ||
![]() |
72dfe974ab | ||
![]() |
316db0e4c6 | ||
![]() |
010c607e40 | ||
![]() |
3e099fb2a3 | ||
![]() |
9c9730b152 | ||
![]() |
9e44053e22 | ||
![]() |
dee32c3dc5 | ||
![]() |
344fbff59a | ||
![]() |
e39a816bdc | ||
![]() |
605b8fac5e | ||
![]() |
dfba10f8ae | ||
![]() |
48a1ab64b0 | ||
![]() |
dd2cde3c1a | ||
![]() |
1b9c2b37c5 | ||
![]() |
eae1f8b597 | ||
![]() |
18ce86c2ed | ||
![]() |
d5f25e05d9 | ||
![]() |
53303ac5d3 | ||
![]() |
90cc8e2144 | ||
![]() |
adf9badbf6 | ||
![]() |
c35fe4f3f1 | ||
![]() |
63291f8101 | ||
![]() |
62efb588ef | ||
![]() |
203ca9afc6 | ||
![]() |
a23f941ac8 | ||
![]() |
b0a10f0542 | ||
![]() |
478ad42977 | ||
![]() |
0764983ac6 | ||
![]() |
2b2f1ee8f5 | ||
![]() |
28f167fd99 | ||
![]() |
272be36dd9 | ||
![]() |
f933db8117 | ||
![]() |
cddb9bccb9 | ||
![]() |
b5ad24eb47 | ||
![]() |
ad8f791f71 | ||
![]() |
2e862b4ccc | ||
![]() |
ecac897e7b | ||
![]() |
702adb53a7 | ||
![]() |
4ea962f523 | ||
![]() |
acaf92d671 | ||
![]() |
c673cb6157 | ||
![]() |
c0f7b123a3 | ||
![]() |
e9e2afa61a | ||
![]() |
403154b2e1 | ||
![]() |
e5fd24b0d1 | ||
![]() |
8dc34274a1 | ||
![]() |
467bd21de2 | ||
![]() |
5c9705d94e | ||
![]() |
85fb5827aa | ||
![]() |
0bcc9bd3ba | ||
![]() |
25e120bec1 | ||
![]() |
7067deb328 | ||
![]() |
f6efd302dc | ||
![]() |
61972141ae | ||
![]() |
af936bc646 | ||
![]() |
d66f933c69 | ||
![]() |
cf81c37683 | ||
![]() |
d2306b0fd7 | ||
![]() |
94dfabf3dc | ||
![]() |
5522dc10b8 | ||
![]() |
0ae04b8ead | ||
![]() |
44cad27d0a | ||
![]() |
5d59025b3c | ||
![]() |
768bb0bbcd | ||
![]() |
ac071b383f | ||
![]() |
e0b1a6b88b | ||
![]() |
ed86b1c572 | ||
![]() |
b6c2bade73 | ||
![]() |
b6b19b474e | ||
![]() |
231b7492fb | ||
![]() |
b4950fcb2e | ||
![]() |
b79ea7b51b | ||
![]() |
28c72e7f63 | ||
![]() |
5fcc3b4dab | ||
![]() |
51837ce36f | ||
![]() |
ddaafb68c8 | ||
![]() |
a744775fe7 | ||
![]() |
50b85a7734 | ||
![]() |
aab09c0c65 | ||
![]() |
3ded6feddb | ||
![]() |
c8802fe5d0 | ||
![]() |
411b3129f9 | ||
![]() |
a55acd38df | ||
![]() |
e7773d8807 | ||
![]() |
7edef8d5a2 | ||
![]() |
03d2ca9f9f | ||
![]() |
2271ea4281 | ||
![]() |
afc8db8f81 | ||
![]() |
4af49ee5a6 | ||
![]() |
d7b29aae5c | ||
![]() |
9f7a8407ca | ||
![]() |
a2050a5211 | ||
![]() |
048743c062 | ||
![]() |
e9bd2934c3 | ||
![]() |
50634eb2b3 | ||
![]() |
08489b81fb | ||
![]() |
a2ff770afc | ||
![]() |
658d988254 | ||
![]() |
9d7e9289bb | ||
![]() |
12aac09c7b | ||
![]() |
d7d87691cb | ||
![]() |
731640997e | ||
![]() |
64d7432852 | ||
![]() |
1c9f68bcae | ||
![]() |
4fde62ff89 | ||
![]() |
ceb55d0ede | ||
![]() |
87c958b2e7 | ||
![]() |
d844e0aba6 | ||
![]() |
a953aab9b4 | ||
![]() |
108af48b76 |
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,15 +1,15 @@
|
||||
.gitignore
|
||||
.gradle
|
||||
/local.properties
|
||||
.gradle/
|
||||
local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/app/app.iml
|
||||
/.idea
|
||||
/*.iml
|
||||
build/
|
||||
captures/
|
||||
.idea/
|
||||
*.iml
|
||||
*~
|
||||
.weblate
|
||||
*.class
|
||||
**/debug/
|
||||
**/release/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.gitignore
|
||||
/build
|
||||
*.iml
|
@@ -17,8 +17,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 978
|
||||
versionName "0.21.12"
|
||||
versionCode 982
|
||||
versionName "0.21.16"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -105,9 +105,9 @@ ext {
|
||||
androidxRoomVersion = '2.3.0'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
exoPlayerVersion = '2.14.2'
|
||||
googleAutoServiceVersion = '1.0'
|
||||
groupieVersion = '2.9.0'
|
||||
groupieVersion = '2.10.0'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
@@ -189,7 +189,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:v0.21.11'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.13'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
@@ -208,14 +208,17 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.media:media:1.3.1'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
|
||||
|
@@ -256,6 +256,21 @@
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- y2u.be filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="y2u.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Soundcloud filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -325,8 +340,12 @@
|
||||
<data android:host="skeptikon.fr" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||
<data android:pathPrefix="/w/p/" /> <!-- short playlist URLs -->
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||
</intent-filter>
|
||||
|
||||
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||
|
@@ -51,8 +51,12 @@ import java.util.ArrayList;
|
||||
* <li>{@link #saveState()}</li>
|
||||
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||
* {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
||||
private static final String TAG = "FragmentStatePagerAdapt";
|
||||
private static final boolean DEBUG = false;
|
||||
@@ -86,9 +90,10 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
private boolean mExecutingFinishUpdate;
|
||||
|
||||
/**
|
||||
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||
@@ -208,7 +213,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
mFragments.set(position, null);
|
||||
|
||||
mCurTransaction.remove(fragment);
|
||||
if (fragment == mCurrentPrimaryItem) {
|
||||
if (fragment.equals(mCurrentPrimaryItem)) {
|
||||
mCurrentPrimaryItem = null;
|
||||
}
|
||||
}
|
||||
@@ -247,7 +252,19 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@Override
|
||||
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||
if (mCurTransaction != null) {
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
// We drop any transactions that attempt to be committed
|
||||
// from a re-entrant call to finishUpdate(). We need to
|
||||
// do this as a workaround for Robolectric running measure/layout
|
||||
// calls inline rather than allowing them to be posted
|
||||
// as they would on a real device.
|
||||
if (!mExecutingFinishUpdate) {
|
||||
try {
|
||||
mExecutingFinishUpdate = true;
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
} finally {
|
||||
mExecutingFinishUpdate = false;
|
||||
}
|
||||
}
|
||||
mCurTransaction = null;
|
||||
}
|
||||
}
|
||||
|
@@ -254,4 +254,5 @@ public class App extends MultiDexApplication {
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ public abstract class BaseFragment extends Fragment {
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public void useAsFrontPage(final boolean value) {
|
||||
useAsFrontPage = value;
|
||||
@@ -85,12 +84,6 @@ public abstract class BaseFragment extends Fragment {
|
||||
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -109,8 +102,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
}
|
||||
if ((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -15,7 +14,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@@ -48,7 +46,8 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private static final String RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
@@ -129,44 +128,37 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
final int notificationId = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(2000, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
public static boolean isReleaseApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
|
||||
}
|
||||
|
||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
||||
@@ -175,9 +167,8 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk(app)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,6 +204,7 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
@@ -235,6 +227,23 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new service which
|
||||
* checks if all conditions for performing a version check are met,
|
||||
* fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
|
||||
* about the latest NewPipe version
|
||||
* and displays a notification about ana available update.
|
||||
* <br>
|
||||
* Following conditions need to be met, before data is request from the server:
|
||||
* <ul>
|
||||
* <li> The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||
* If the signing key differs from the one used upstream, the update cannot be installed.</li>
|
||||
* <li>The user enabled searching for and notifying about updates in the settings.</li>
|
||||
* <li>The app did not recently check for updates.
|
||||
* We do not want to make unnecessary connections and DOS our servers.</li>
|
||||
* </ul>
|
||||
* <b>Must not be executed</b> when the app is in background.
|
||||
*/
|
||||
public static void startNewVersionCheckService() {
|
||||
final Intent intent = new Intent(App.getApp().getApplicationContext(),
|
||||
CheckForNewAppVersion.class);
|
||||
|
@@ -21,6 +21,7 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@@ -93,8 +94,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@@ -165,12 +164,59 @@ public class MainActivity extends AppCompatActivity {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
|
||||
// Check for new version
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||
// Start the service which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
// The service searching for a new NewPipe version must not be started in background.
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawer() throws ExtractionException {
|
||||
addDrawerMenuForCurrentService();
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the drawer menu for the current service.
|
||||
*
|
||||
* @throws ExtractionException
|
||||
*/
|
||||
private void addDrawerMenuForCurrentService() throws ExtractionException {
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
@@ -209,32 +255,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
private boolean drawerItemSelected(final MenuItem item) {
|
||||
@@ -342,11 +362,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
// Show up or down arrow
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(
|
||||
servicesShown ? R.drawable.ic_arrow_drop_up : R.drawable.ic_arrow_drop_down);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
showTabs();
|
||||
addDrawerMenuForCurrentService();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
|
||||
}
|
||||
@@ -354,8 +378,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up);
|
||||
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
@@ -419,48 +441,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
menuItem.setActionView(spinner);
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(R.drawable.ic_tv);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(R.drawable.ic_rss_feed);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(R.drawable.ic_bookmark);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(R.drawable.ic_file_download);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(R.drawable.ic_history);
|
||||
|
||||
//Settings and About
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
@@ -9,8 +9,8 @@ import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
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;
|
||||
@@ -18,6 +18,9 @@ import org.schabi.newpipe.util.NavigationHelper;
|
||||
import java.util.Collections;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
private QueueItemMenuUtil() {
|
||||
}
|
||||
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
@@ -47,13 +50,22 @@ public final class QueueItemMenuUtil {
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
|
||||
Collections.singletonList(item)
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
Collections.singletonList(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragmentManager,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
)
|
||||
);
|
||||
PlaylistAppendDialog.onPlaylistFound(context,
|
||||
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
|
||||
() -> PlaylistCreationDialog.newInstance(d)
|
||||
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
|
||||
|
||||
return true;
|
||||
case R.id.menu_item_channel_details:
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
NavigationHelper.openChannelFragmentUsingIntent(context, item.getServiceId(),
|
||||
item.getUploaderUrl(), item.getUploader());
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
@@ -65,6 +77,4 @@ public final class QueueItemMenuUtil {
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
private QueueItemMenuUtil() { }
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
@@ -30,6 +33,7 @@ import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
@@ -56,6 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
@@ -69,14 +74,15 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
@@ -89,9 +95,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
|
||||
/**
|
||||
* Get the url from the intent and open it in the chosen preferred player.
|
||||
*/
|
||||
@@ -107,6 +110,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
protected String currentUrl;
|
||||
private StreamingService currentService;
|
||||
private boolean selectionIsDownload = false;
|
||||
private boolean selectionIsAddToPlaylist = false;
|
||||
private AlertDialog alertDialogChoice = null;
|
||||
|
||||
@Override
|
||||
@@ -350,7 +354,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> {
|
||||
if (!selectionIsDownload) {
|
||||
if (!selectionIsDownload && !selectionIsAddToPlaylist) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
@@ -446,6 +450,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
R.drawable.ic_headset);
|
||||
final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem(
|
||||
getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist),
|
||||
R.drawable.ic_add);
|
||||
|
||||
|
||||
if (linkType == LinkType.STREAM) {
|
||||
if (isExtVideoEnabled) {
|
||||
@@ -482,6 +490,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
getString(R.string.download),
|
||||
R.drawable.ic_file_download));
|
||||
|
||||
// Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
|
||||
// not be added to a playlist
|
||||
returnList.add(addToPlaylist);
|
||||
|
||||
} else {
|
||||
returnList.add(showInfo);
|
||||
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
|
||||
@@ -547,6 +559,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) {
|
||||
selectionIsAddToPlaylist = true;
|
||||
openAddToPlaylistDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// stop and bypass FetcherService if InfoScreen was selected since
|
||||
// StreamDetailFragment can fetch data itself
|
||||
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
@@ -572,6 +590,41 @@ public class RouterActivity extends AppCompatActivity {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog() {
|
||||
// Getting the stream info usually takes a moment
|
||||
// Notifying the user here to ensure that no confusion arises
|
||||
Toast.makeText(
|
||||
getApplicationContext(),
|
||||
getString(R.string.processing_may_take_a_moment),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
info -> PlaylistDialog.createCorrespondingDialog(
|
||||
getThemeWrapperContext(),
|
||||
Collections.singletonList(new StreamEntity(info)),
|
||||
playlistDialog -> {
|
||||
playlistDialog.setOnDismissListener(dialog -> finish());
|
||||
|
||||
playlistDialog.show(
|
||||
this.getSupportFragmentManager(),
|
||||
"addToPlaylistDialog"
|
||||
);
|
||||
}
|
||||
),
|
||||
throwable -> handleError(this, new ErrorInfo(
|
||||
throwable,
|
||||
UserAction.REQUESTED_STREAM,
|
||||
"Tried to add " + currentUrl + " to a playlist",
|
||||
currentService.getServiceId())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog() {
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
|
@@ -7,6 +7,7 @@ import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
@@ -37,7 +38,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
|
||||
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -62,7 +63,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
||||
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
@@ -97,7 +98,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
|
||||
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
@@ -137,7 +138,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
@@ -0,0 +1,103 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ensures that a Exception is serializable.
|
||||
* This is
|
||||
*/
|
||||
public final class EnsureExceptionSerializable {
|
||||
private static final String TAG = "EnsureExSerializable";
|
||||
|
||||
private EnsureExceptionSerializable() {
|
||||
// No instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an exception is serializable.
|
||||
* <br/>
|
||||
* If that is not the case a {@link WorkaroundNotSerializableException} is created.
|
||||
*
|
||||
* @param exception
|
||||
* @return if an exception is not serializable a new {@link WorkaroundNotSerializableException}
|
||||
* otherwise the exception from the parameter
|
||||
*/
|
||||
public static Exception ensureSerializable(@NonNull final Exception exception) {
|
||||
return checkIfSerializable(exception)
|
||||
? exception
|
||||
: WorkaroundNotSerializableException.create(exception);
|
||||
}
|
||||
|
||||
public static boolean checkIfSerializable(@NonNull final Exception exception) {
|
||||
try {
|
||||
// Check by creating a new ObjectOutputStream which does the serialization
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(bos)
|
||||
) {
|
||||
oos.writeObject(exception);
|
||||
oos.flush();
|
||||
|
||||
bos.toByteArray();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (final IOException ex) {
|
||||
Log.d(TAG, "Exception is not serializable", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorkaroundNotSerializableException extends Exception {
|
||||
protected WorkaroundNotSerializableException(
|
||||
final Throwable notSerializableException,
|
||||
final Throwable cause) {
|
||||
super(notSerializableException.toString(), cause);
|
||||
setStackTrace(notSerializableException.getStackTrace());
|
||||
}
|
||||
|
||||
protected WorkaroundNotSerializableException(final Throwable notSerializableException) {
|
||||
super(notSerializableException.toString());
|
||||
setStackTrace(notSerializableException.getStackTrace());
|
||||
}
|
||||
|
||||
public static WorkaroundNotSerializableException create(
|
||||
@NonNull final Exception notSerializableException
|
||||
) {
|
||||
// Build a list of the exception + all causes
|
||||
final List<Throwable> throwableList = new ArrayList<>();
|
||||
|
||||
int pos = 0;
|
||||
Throwable throwableToProcess = notSerializableException;
|
||||
|
||||
while (throwableToProcess != null) {
|
||||
throwableList.add(throwableToProcess);
|
||||
|
||||
pos++;
|
||||
throwableToProcess = throwableToProcess.getCause();
|
||||
}
|
||||
|
||||
// Reverse list so that it starts with the last one
|
||||
Collections.reverse(throwableList);
|
||||
|
||||
// Build exception stack
|
||||
WorkaroundNotSerializableException cause = null;
|
||||
for (final Throwable t : throwableList) {
|
||||
cause = cause == null
|
||||
? new WorkaroundNotSerializableException(t)
|
||||
: new WorkaroundNotSerializableException(t, cause);
|
||||
}
|
||||
|
||||
return cause;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -77,6 +77,16 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
/**
|
||||
* Reports a new error by starting a new activity.
|
||||
* <br/>
|
||||
* Ensure that the data within errorInfo is serializable otherwise
|
||||
* an exception will be thrown!<br/>
|
||||
* {@link EnsureExceptionSerializable} might help.
|
||||
*
|
||||
* @param context
|
||||
* @param errorInfo
|
||||
*/
|
||||
public static void reportError(final Context context, final ErrorInfo errorInfo) {
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
|
@@ -20,8 +20,8 @@ public class BlankFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setTitle("NewPipe");
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
|
@@ -52,6 +52,7 @@ import com.squareup.picasso.Callback;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
@@ -73,8 +74,7 @@ import org.schabi.newpipe.fragments.EmptyFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
@@ -99,6 +99,7 @@ import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -444,12 +445,11 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_controls_playlist_append:
|
||||
if (getFM() != null && currentInfo != null) {
|
||||
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo);
|
||||
disposables.add(
|
||||
PlaylistAppendDialog.onPlaylistFound(getContext(),
|
||||
() -> d.show(getFM(), TAG),
|
||||
() -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG)
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
Collections.singletonList(new StreamEntity(currentInfo)),
|
||||
dialog -> dialog.show(getFM(), TAG)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -594,6 +594,11 @@ public final class VideoDetailFragment
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
@@ -604,6 +609,18 @@ public final class VideoDetailFragment
|
||||
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
|
||||
binding.detailControlsPlayWithKodi.setVisibility(
|
||||
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
binding.detailControlsCrashThePlayer.setVisibility(
|
||||
DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getBoolean(getString(R.string.show_crash_the_player_key), false)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
// remove ripple effects from detail controls
|
||||
final int transparent = ContextCompat.getColor(requireContext(),
|
||||
@@ -638,8 +655,14 @@ public final class VideoDetailFragment
|
||||
binding.detailControlsShare.setOnClickListener(this);
|
||||
binding.detailControlsOpenInBrowser.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
|
||||
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
|
||||
if (DEBUG) {
|
||||
binding.detailControlsCrashThePlayer.setOnClickListener(
|
||||
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
||||
this.getContext(),
|
||||
this.player,
|
||||
getLayoutInflater())
|
||||
);
|
||||
}
|
||||
|
||||
binding.overlayThumbnail.setOnClickListener(this);
|
||||
binding.overlayThumbnail.setOnLongClickListener(this);
|
||||
@@ -1181,7 +1204,7 @@ public final class VideoDetailFragment
|
||||
addVideoPlayerView();
|
||||
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, autoPlayEnabled);
|
||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,159 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||
*/
|
||||
public final class VideoDetailPlayerCrasher {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
private static final String TAG = "VideoDetPlayerCrasher";
|
||||
|
||||
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
|
||||
getExceptionTypes();
|
||||
|
||||
private VideoDetailPlayerCrasher() {
|
||||
// No impls
|
||||
}
|
||||
|
||||
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
|
||||
final String defaultMsg = "Dummy";
|
||||
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
|
||||
exceptionTypes.put(
|
||||
"Source",
|
||||
() -> ExoPlaybackException.createForSource(
|
||||
new IOException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Renderer",
|
||||
() -> ExoPlaybackException.createForRenderer(
|
||||
new Exception(defaultMsg),
|
||||
"Dummy renderer",
|
||||
0,
|
||||
null,
|
||||
C.FORMAT_HANDLED
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Unexpected",
|
||||
() -> ExoPlaybackException.createForUnexpected(
|
||||
new RuntimeException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Remote",
|
||||
() -> ExoPlaybackException.createForRemote(defaultMsg)
|
||||
);
|
||||
|
||||
return Collections.unmodifiableMap(exceptionTypes);
|
||||
}
|
||||
|
||||
private static Context getThemeWrapperContext(final Context context) {
|
||||
return new ContextThemeWrapper(
|
||||
context,
|
||||
ThemeHelper.isLightThemeSelected(context)
|
||||
? R.style.LightTheme
|
||||
: R.style.DarkTheme);
|
||||
}
|
||||
|
||||
public static void onCrashThePlayer(
|
||||
@NonNull final Context context,
|
||||
@Nullable final Player player,
|
||||
@NonNull final LayoutInflater layoutInflater
|
||||
) {
|
||||
if (player == null) {
|
||||
Log.d(TAG, "Player is not available");
|
||||
Toast.makeText(context, "Player is not available", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Build the dialog/UI --
|
||||
|
||||
final Context themeWrapperContext = getThemeWrapperContext(context);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
|
||||
.list;
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
|
||||
.setTitle("Choose an exception")
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
|
||||
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(entry.getKey());
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setLayoutParams(
|
||||
new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
);
|
||||
radioButton.setOnClickListener(v -> {
|
||||
tryCrashPlayerWith(player, entry.getValue().get());
|
||||
if (alertDialog != null) {
|
||||
alertDialog.cancel();
|
||||
}
|
||||
});
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
|
||||
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
|
||||
* @param player
|
||||
* @param exception
|
||||
*/
|
||||
private static void tryCrashPlayerWith(
|
||||
@NonNull final Player player,
|
||||
@NonNull final ExoPlaybackException exception
|
||||
) {
|
||||
Log.d(TAG, "Crashing the player using player.onPlayerError(ex)");
|
||||
try {
|
||||
player.onPlayerError(exception);
|
||||
} catch (final Exception exPlayer) {
|
||||
Log.e(TAG,
|
||||
"Run into an exception while crashing the player:",
|
||||
exPlayer);
|
||||
}
|
||||
}
|
||||
}
|
@@ -143,7 +143,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
final View focusedItem = itemsList.getFocusedChild();
|
||||
final RecyclerView.ViewHolder itemHolder =
|
||||
itemsList.findContainingViewHolder(focusedItem);
|
||||
return itemHolder.getAdapterPosition();
|
||||
return itemHolder.getBindingAdapterPosition();
|
||||
} catch (final NullPointerException e) {
|
||||
return -1;
|
||||
}
|
||||
@@ -378,6 +378,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
|
@@ -98,11 +98,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null && useAsFrontPage) {
|
||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||
}
|
||||
}
|
||||
|
@@ -99,9 +99,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (useAsFrontPage && isVisibleToUser && activity != null) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
if (useAsFrontPage && activity != null) {
|
||||
try {
|
||||
setTitle(kioskTranslatedName);
|
||||
} catch (final Exception e) {
|
||||
@@ -117,15 +120,6 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@@ -176,6 +176,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
|
@@ -1088,7 +1088,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1099,7 +1099,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
final String query = suggestionListAdapter.getItem(position).query;
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@@ -299,18 +299,36 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||
}
|
||||
}
|
||||
|
||||
fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
|
||||
) {
|
||||
slideUp(duration, delay, translationPercent, null)
|
||||
}
|
||||
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long = 0L,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
|
||||
animate().setListener(null).cancel()
|
||||
alpha = 0f
|
||||
translationY = newTranslationY.toFloat()
|
||||
visibility = View.VISIBLE
|
||||
isVisible = true
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
|
@@ -78,9 +78,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null) {
|
||||
setTitle(activity.getString(R.string.tab_bookmarks));
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -17,20 +16,14 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
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.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||
@@ -40,47 +33,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
|
||||
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
public static Disposable onPlaylistFound(
|
||||
final Context context, final Runnable onSuccess, final Runnable onFailed
|
||||
) {
|
||||
final LocalPlaylistManager playlistManager =
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
|
||||
|
||||
return playlistManager.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(hasPlaylists -> {
|
||||
if (hasPlaylists) {
|
||||
onSuccess.run();
|
||||
} else {
|
||||
onFailed.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
final List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||
for (final StreamInfoItem item : items) {
|
||||
entities.add(new StreamEntity(item));
|
||||
}
|
||||
dialog.setInfo(entities);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
final List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||
for (final PlayQueueItem item : items) {
|
||||
entities.add(new StreamEntity(item));
|
||||
}
|
||||
dialog.setInfo(entities);
|
||||
return dialog;
|
||||
public PlaylistAppendDialog(final List<StreamEntity> streamEntities) {
|
||||
super(streamEntities);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -104,11 +58,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry)
|
||||
|| getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
|
||||
getStreams());
|
||||
onPlaylistSelected(
|
||||
playlistManager,
|
||||
(PlaylistMetadataEntry) selectedItem,
|
||||
getStreamEntities()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,11 +104,17 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void openCreatePlaylistDialog() {
|
||||
if (getStreams() == null || !isAdded()) {
|
||||
if (getStreamEntities() == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
|
||||
final PlaylistCreationDialog playlistCreationDialog =
|
||||
new PlaylistCreationDialog(getStreamEntities());
|
||||
// Move the dismissListener to the new dialog.
|
||||
playlistCreationDialog.setOnDismissListener(this.getOnDismissListener());
|
||||
this.setOnDismissListener(null);
|
||||
|
||||
playlistCreationDialog.show(getParentFragmentManager(), TAG);
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
||||
@@ -165,7 +129,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@NonNull final PlaylistMetadataEntry playlist,
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
if (getStreams() == null) {
|
||||
if (getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -7,29 +7,22 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog.Builder;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
|
||||
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
|
||||
dialog.setInfo(streams);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistCreationDialog newInstance(final PlaylistAppendDialog appendDialog) {
|
||||
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
|
||||
dialog.setInfo(appendDialog.getStreams());
|
||||
return dialog;
|
||||
public PlaylistCreationDialog(final List<StreamEntity> streamEntities) {
|
||||
super(streamEntities);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -39,16 +32,18 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||
if (getStreams() == null) {
|
||||
if (getStreamEntities() == null) {
|
||||
return super.onCreateDialog(savedInstanceState);
|
||||
}
|
||||
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
|
||||
final Builder dialogBuilder = new Builder(requireContext(),
|
||||
ThemeHelper.getDialogTheme(requireContext()))
|
||||
.setTitle(R.string.create_playlist)
|
||||
.setView(dialogBinding.getRoot())
|
||||
.setCancelable(true)
|
||||
@@ -61,11 +56,10 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
R.string.playlist_creation_success,
|
||||
Toast.LENGTH_SHORT);
|
||||
|
||||
playlistManager.createPlaylist(name, getStreams())
|
||||
playlistManager.createPlaylist(name, getStreamEntities())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(longs -> successToast.show());
|
||||
});
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
|
||||
@@ -8,23 +10,29 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
|
||||
|
||||
@Nullable
|
||||
private DialogInterface.OnDismissListener onDismissListener = null;
|
||||
|
||||
private List<StreamEntity> streamEntities;
|
||||
|
||||
private org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
protected void setInfo(final List<StreamEntity> entities) {
|
||||
this.streamEntities = entities;
|
||||
}
|
||||
|
||||
protected List<StreamEntity> getStreams() {
|
||||
return streamEntities;
|
||||
public PlaylistDialog(final List<StreamEntity> streamEntities) {
|
||||
this.streamEntities = streamEntities;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -43,6 +51,10 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
StateSaver.onDestroy(savedState);
|
||||
}
|
||||
|
||||
public List<StreamEntity> getStreamEntities() {
|
||||
return streamEntities;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||
@@ -55,6 +67,14 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull final DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
if (onDismissListener != null) {
|
||||
onDismissListener.onDismiss(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -84,4 +104,47 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
savedState, outState, this);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getter + Setter
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Nullable
|
||||
public DialogInterface.OnDismissListener getOnDismissListener() {
|
||||
return onDismissListener;
|
||||
}
|
||||
|
||||
public void setOnDismissListener(
|
||||
@Nullable final DialogInterface.OnDismissListener onDismissListener
|
||||
) {
|
||||
this.onDismissListener = onDismissListener;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Dialog creation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Creates a {@link PlaylistAppendDialog} when playlists exists,
|
||||
* otherwise a {@link PlaylistCreationDialog}.
|
||||
*
|
||||
* @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 Disposable
|
||||
*/
|
||||
public static Disposable createCorrespondingDialog(
|
||||
final Context context,
|
||||
final List<StreamEntity> streamEntities,
|
||||
final Consumer<PlaylistDialog> onExec
|
||||
) {
|
||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||
.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(hasPlaylists ->
|
||||
onExec.accept(hasPlaylists
|
||||
? new PlaylistAppendDialog(streamEntities)
|
||||
: new PlaylistCreationDialog(streamEntities))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
|
||||
fun getStreams(
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
getPlayedStreams: Boolean = true
|
||||
): Flowable<List<StreamWithState>> {
|
||||
): Maybe<List<StreamWithState>> {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
||||
if (getPlayedStreams) feedTable.getAllStreams()
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,18 @@
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
@@ -23,19 +26,16 @@ import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(
|
||||
applicationContext: Context,
|
||||
private val applicationContext: Context,
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
initialShowPlayedItems: Boolean = true
|
||||
) : ViewModel() {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||
|
||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val streamItems = toggleShowPlayedItems
|
||||
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||
.startWithItem(initialShowPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
.switchMap { showPlayedItems ->
|
||||
feedDatabaseManager.getStreams(groupId, showPlayedItems)
|
||||
}
|
||||
|
||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
@@ -43,17 +43,28 @@ class FeedViewModel(
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
streamItems,
|
||||
toggleShowPlayedItemsFlowable,
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
|
||||
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
||||
t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
||||
var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||
feedDatabaseManager
|
||||
.getStreams(groupId, showPlayedItems)
|
||||
.blockingGet(arrayListOf())
|
||||
else
|
||||
arrayListOf()
|
||||
|
||||
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
mutableStateLiveData.postValue(
|
||||
@@ -75,20 +86,50 @@ class FeedViewModel(
|
||||
combineDisposable.dispose()
|
||||
}
|
||||
|
||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
|
||||
private data class CombineResultEventHolder(
|
||||
val t1: FeedEventManager.Event,
|
||||
val t2: Boolean,
|
||||
val t3: Long,
|
||||
val t4: OffsetDateTime?
|
||||
)
|
||||
|
||||
private data class CombineResultDataHolder(
|
||||
val t1: FeedEventManager.Event,
|
||||
val t2: List<StreamWithState>,
|
||||
val t3: Long,
|
||||
val t4: OffsetDateTime?
|
||||
)
|
||||
|
||||
fun togglePlayedItems(showPlayedItems: Boolean) {
|
||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||
}
|
||||
|
||||
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
|
||||
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||
this.apply()
|
||||
}
|
||||
|
||||
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
|
||||
|
||||
companion object {
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
private val showPlayedItems: Boolean
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
|
||||
return FeedViewModel(
|
||||
context.applicationContext,
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
data class StreamItem(
|
||||
val streamWithState: StreamWithState,
|
||||
@@ -31,6 +32,12 @@ data class StreamItem(
|
||||
private val stream: StreamEntity = streamWithState.stream
|
||||
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
||||
|
||||
/**
|
||||
* Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
|
||||
* Can be used e.g. for highlighting a item.
|
||||
*/
|
||||
var execBindEnd: Consumer<ListStreamItemBinding>? = null
|
||||
|
||||
override fun getId(): Long = stream.uid
|
||||
|
||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||
@@ -97,6 +104,8 @@ data class StreamItem(
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||
}
|
||||
|
||||
execBindEnd?.accept(viewBinding)
|
||||
}
|
||||
|
||||
override fun isLongClickable() = when (stream.streamType) {
|
||||
|
@@ -120,19 +120,11 @@ public class HistoryRecordManager {
|
||||
}
|
||||
|
||||
// Update the stream progress to the full duration of the video
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
||||
.blockingFirst();
|
||||
if (!states.isEmpty()) {
|
||||
final StreamStateEntity entity = states.get(0);
|
||||
entity.setProgressMillis(duration * 1000);
|
||||
streamStateTable.update(entity);
|
||||
} else {
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.insert(entity);
|
||||
}
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.upsert(entity);
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
@@ -334,9 +326,9 @@ public class HistoryRecordManager {
|
||||
.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
} else {
|
||||
result.add(states.get(0));
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
@@ -362,9 +354,9 @@ public class HistoryRecordManager {
|
||||
.blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
} else {
|
||||
result.add(states.get(0));
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user