Merge branch 'dev' into pr2335
This commit is contained in:
commit
6e8c9f92cb
423 changed files with 3413 additions and 4301 deletions
8
LICENSE
8
LICENSE
|
@ -1,7 +1,7 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|
|
@ -9,15 +9,15 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 31
|
compileSdk 31
|
||||||
buildToolsVersion '30.0.3'
|
buildToolsVersion '31.0.0'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.schabi.newpipe"
|
applicationId "org.schabi.newpipe"
|
||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdk 19
|
minSdk 19
|
||||||
targetSdk 29
|
targetSdk 29
|
||||||
versionCode 983
|
versionCode 984
|
||||||
versionName "0.22.0"
|
versionName "0.22.1"
|
||||||
|
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
|
@ -98,10 +98,10 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
checkstyleVersion = '9.2.1'
|
checkstyleVersion = '10.0'
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.3.1'
|
androidxLifecycleVersion = '2.3.1'
|
||||||
androidxRoomVersion = '2.3.0'
|
androidxRoomVersion = '2.4.2'
|
||||||
androidxWorkVersion = '2.7.1'
|
androidxWorkVersion = '2.7.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
|
@ -122,7 +122,7 @@ configurations {
|
||||||
}
|
}
|
||||||
|
|
||||||
checkstyle {
|
checkstyle {
|
||||||
getConfigDirectory().set(rootProject.file("."))
|
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||||
ignoreFailures false
|
ignoreFailures false
|
||||||
showViolations true
|
showViolations true
|
||||||
toolVersion = checkstyleVersion
|
toolVersion = checkstyleVersion
|
||||||
|
@ -194,7 +194,7 @@ dependencies {
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
ktlint 'com.pinterest:ktlint:0.43.2'
|
ktlint 'com.pinterest:ktlint:0.44.0'
|
||||||
|
|
||||||
/** Kotlin **/
|
/** Kotlin **/
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||||
|
@ -202,16 +202,16 @@ dependencies {
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.core:core-ktx:1.6.0'
|
implementation 'androidx.core:core-ktx:1.6.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.media:media:1.4.3'
|
implementation 'androidx.media:media:1.5.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.2.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||||
|
@ -221,7 +221,8 @@ dependencies {
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.webkit:webkit:1.4.0'
|
implementation 'androidx.webkit:webkit:1.4.0'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
|
implementation "androidx.work:work-runtime:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
|
|
||||||
|
@ -249,8 +250,6 @@ dependencies {
|
||||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
||||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||||
|
|
||||||
// Circular ImageView
|
|
||||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
|
||||||
// Image loading
|
// Image loading
|
||||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||||
implementation "com.squareup.picasso:picasso:2.8"
|
implementation "com.squareup.picasso:picasso:2.8"
|
||||||
|
|
|
@ -381,9 +381,6 @@
|
||||||
<service
|
<service
|
||||||
android:name=".RouterActivity$FetcherService"
|
android:name=".RouterActivity$FetcherService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
|
||||||
android:name=".CheckForNewAppVersion"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<!-- opting out of sending metrics to Google in Android System WebView -->
|
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||||
|
|
|
@ -1,264 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.app.IntentService;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.Signature;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
|
||||||
import com.grack.nanojson.JsonParser;
|
|
||||||
import com.grack.nanojson.JsonParserException;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Response;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class CheckForNewAppVersion extends IntentService {
|
|
||||||
public CheckForNewAppVersion() {
|
|
||||||
super("CheckForNewAppVersion");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
|
||||||
|
|
||||||
// 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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
|
||||||
*
|
|
||||||
* @param application The application
|
|
||||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
|
||||||
final List<Signature> signatures;
|
|
||||||
try {
|
|
||||||
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
|
|
||||||
application.getPackageName());
|
|
||||||
} catch (final PackageManager.NameNotFoundException e) {
|
|
||||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
|
||||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (signatures.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
final X509Certificate c;
|
|
||||||
try {
|
|
||||||
final byte[] cert = signatures.get(0).toByteArray();
|
|
||||||
final InputStream input = new ByteArrayInputStream(cert);
|
|
||||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
|
||||||
c = (X509Certificate) cf.generateCertificate(input);
|
|
||||||
} catch (final CertificateException e) {
|
|
||||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
|
||||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
final byte[] publicKey = md.digest(c.getEncoded());
|
|
||||||
return byte2HexFormatted(publicKey);
|
|
||||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
|
||||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
|
||||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String byte2HexFormatted(final byte[] arr) {
|
|
||||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
|
||||||
|
|
||||||
for (int i = 0; i < arr.length; i++) {
|
|
||||||
String h = Integer.toHexString(arr[i]);
|
|
||||||
final int l = h.length();
|
|
||||||
if (l == 1) {
|
|
||||||
h = "0" + h;
|
|
||||||
}
|
|
||||||
if (l > 2) {
|
|
||||||
h = h.substring(l - 2, l);
|
|
||||||
}
|
|
||||||
str.append(h.toUpperCase());
|
|
||||||
if (i < (arr.length - 1)) {
|
|
||||||
str.append(':');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to compare the current and latest available app version.
|
|
||||||
* If a newer version is available, we show the update notification.
|
|
||||||
*
|
|
||||||
* @param application The application
|
|
||||||
* @param versionName Name of new version
|
|
||||||
* @param apkLocationUrl Url with the new apk
|
|
||||||
* @param versionCode Code of new version
|
|
||||||
*/
|
|
||||||
private static void compareAppVersionAndShowNotification(@NonNull final Application application,
|
|
||||||
final String versionName,
|
|
||||||
final String apkLocationUrl,
|
|
||||||
final int versionCode) {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isReleaseApk(@NonNull final App app) {
|
|
||||||
return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
|
||||||
final App app = App.getApp();
|
|
||||||
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
|
||||||
final NewVersionManager manager = new NewVersionManager();
|
|
||||||
|
|
||||||
// Check if the current apk is a github one or not.
|
|
||||||
if (!isReleaseApk(app)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the last request has happened a certain time ago
|
|
||||||
// to reduce the number of API requests.
|
|
||||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
|
||||||
if (!manager.isExpired(expiry)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a network request to get latest NewPipe data.
|
|
||||||
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
|
||||||
handleResponse(response, manager, prefs, app);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleResponse(@NonNull final Response response,
|
|
||||||
@NonNull final NewVersionManager manager,
|
|
||||||
@NonNull final SharedPreferences prefs,
|
|
||||||
@NonNull final App app) {
|
|
||||||
try {
|
|
||||||
// Store a timestamp which needs to be exceeded,
|
|
||||||
// before a new request to the API is made.
|
|
||||||
final long newExpiry = manager
|
|
||||||
.coerceExpiry(response.getHeader("expires"));
|
|
||||||
prefs.edit()
|
|
||||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
|
||||||
.apply();
|
|
||||||
} catch (final Exception e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the json from the response.
|
|
||||||
try {
|
|
||||||
|
|
||||||
final JsonObject githubStableObject = JsonParser.object()
|
|
||||||
.from(response.responseBody()).getObject("flavors")
|
|
||||||
.getObject("github").getObject("stable");
|
|
||||||
|
|
||||||
final String versionName = githubStableObject
|
|
||||||
.getString("version");
|
|
||||||
final int versionCode = githubStableObject
|
|
||||||
.getInt("version_code");
|
|
||||||
final String apkLocationUrl = githubStableObject
|
|
||||||
.getString("apk");
|
|
||||||
|
|
||||||
compareAppVersionAndShowNotification(app, versionName,
|
|
||||||
apkLocationUrl, versionCode);
|
|
||||||
} catch (final JsonParserException e) {
|
|
||||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
|
||||||
// Do not alarm user and fail silently.
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
App.getApp().startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onHandleIntent(@Nullable final Intent intent) {
|
|
||||||
try {
|
|
||||||
checkNewVersion();
|
|
||||||
} catch (final IOException e) {
|
|
||||||
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
|
|
||||||
} catch (final ReCaptchaException e) {
|
|
||||||
Log.e(TAG, "ReCaptchaException should never happen here.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,7 +20,6 @@
|
||||||
|
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
|
@ -178,10 +177,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||||
// Start the service which is checking all conditions
|
// Start the worker which is checking all conditions
|
||||||
// and eventually searching for a new version.
|
// and eventually searching for a new version.
|
||||||
// The service searching for a new NewPipe version must not be started in background.
|
NewVersionWorker.enqueueNewVersionCheckingWork(app);
|
||||||
startNewVersionCheckService();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +229,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||||
.getTranslatedKioskName(ks, this))
|
.getTranslatedKioskName(ks, this))
|
||||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
.setIcon(KioskTranslator.getKioskIcon(ks));
|
||||||
kioskId++;
|
kioskId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package org.schabi.newpipe
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
class NewVersionManager {
|
|
||||||
|
|
||||||
fun isExpired(expiry: Long): Boolean {
|
|
||||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coerce expiry date time in between 6 hours and 72 hours from now
|
|
||||||
*
|
|
||||||
* @return Epoch second of expiry date time
|
|
||||||
*/
|
|
||||||
fun coerceExpiry(expiryString: String?): Long {
|
|
||||||
val now = ZonedDateTime.now()
|
|
||||||
return expiryString?.let {
|
|
||||||
|
|
||||||
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
|
||||||
expiry = maxOf(expiry, now.plusHours(6))
|
|
||||||
expiry = minOf(expiry, now.plusHours(72))
|
|
||||||
expiry.toEpochSecond()
|
|
||||||
} ?: now.plusHours(6).toEpochSecond()
|
|
||||||
}
|
|
||||||
}
|
|
163
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
163
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkRequest
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.grack.nanojson.JsonParser
|
||||||
|
import com.grack.nanojson.JsonParserException
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NewVersionWorker(
|
||||||
|
context: Context,
|
||||||
|
workerParams: WorkerParameters
|
||||||
|
) : Worker(context, workerParams) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to compare the current and latest available app version.
|
||||||
|
* If a newer version is available, we show the update notification.
|
||||||
|
*
|
||||||
|
* @param versionName Name of new version
|
||||||
|
* @param apkLocationUrl Url with the new apk
|
||||||
|
* @param versionCode Code of new version
|
||||||
|
*/
|
||||||
|
private fun compareAppVersionAndShowNotification(
|
||||||
|
versionName: String,
|
||||||
|
apkLocationUrl: String?,
|
||||||
|
versionCode: Int
|
||||||
|
) {
|
||||||
|
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val app = App.getApp()
|
||||||
|
|
||||||
|
// A pending intent to open the apk location url in the browser.
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0)
|
||||||
|
val channelId = app.getString(R.string.app_update_notification_channel_id)
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(app, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
|
||||||
|
.setContentText(
|
||||||
|
app.getString(R.string.app_update_notification_content_text) +
|
||||||
|
" " + versionName
|
||||||
|
)
|
||||||
|
val notificationManager = NotificationManagerCompat.from(app)
|
||||||
|
notificationManager.notify(2000, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
|
private fun checkNewVersion() {
|
||||||
|
// Check if the current apk is a github one or not.
|
||||||
|
if (!isReleaseApk()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
// Check if the last request has happened a certain time ago
|
||||||
|
// to reduce the number of API requests.
|
||||||
|
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||||
|
if (!isLastUpdateCheckExpired(expiry)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a network request to get latest NewPipe data.
|
||||||
|
val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL)
|
||||||
|
handleResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResponse(response: Response) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
try {
|
||||||
|
// Store a timestamp which needs to be exceeded,
|
||||||
|
// before a new request to the API is made.
|
||||||
|
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
|
prefs.edit {
|
||||||
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Could not extract and save new expiry date", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the json from the response.
|
||||||
|
try {
|
||||||
|
val githubStableObject = JsonParser.`object`()
|
||||||
|
.from(response.responseBody()).getObject("flavors")
|
||||||
|
.getObject("github").getObject("stable")
|
||||||
|
|
||||||
|
val versionName = githubStableObject.getString("version")
|
||||||
|
val versionCode = githubStableObject.getInt("version_code")
|
||||||
|
val apkLocationUrl = githubStableObject.getString("apk")
|
||||||
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||||
|
} catch (e: JsonParserException) {
|
||||||
|
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||||
|
// Do not alarm user and fail silently.
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Could not get NewPipe API: invalid json", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
try {
|
||||||
|
checkNewVersion()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e)
|
||||||
|
return Result.failure()
|
||||||
|
} catch (e: ReCaptchaException) {
|
||||||
|
Log.e(TAG, "ReCaptchaException should never happen here.", e)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
private val TAG = NewVersionWorker::class.java.simpleName
|
||||||
|
private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new worker which
|
||||||
|
* checks if all conditions for performing a version check are met,
|
||||||
|
* fetches the API endpoint [.NEWPIPE_API_URL] containing info
|
||||||
|
* about the latest NewPipe version
|
||||||
|
* and displays a notification about ana available update.
|
||||||
|
* <br></br>
|
||||||
|
* Following conditions need to be met, before data is request from the server:
|
||||||
|
*
|
||||||
|
* * 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.
|
||||||
|
* * The user enabled searching for and notifying about updates in the settings.
|
||||||
|
* * The app did not recently check for updates.
|
||||||
|
* We do not want to make unnecessary connections and DOS our servers.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun enqueueNewVersionCheckingWork(context: Context) {
|
||||||
|
val workRequest: WorkRequest =
|
||||||
|
OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build()
|
||||||
|
WorkManager.getInstance(context).enqueue(workRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.SaveUploaderUrlHelper;
|
import org.schabi.newpipe.util.SparseItemUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@ -62,7 +62,8 @@ public final class QueueItemMenuUtil {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_channel_details:
|
case R.id.menu_item_channel_details:
|
||||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item,
|
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||||
|
item.getUrl(), item.getUploaderUrl(),
|
||||||
// An intent must be used here.
|
// An intent must be used here.
|
||||||
// Opening with FragmentManager transactions is not working,
|
// Opening with FragmentManager transactions is not working,
|
||||||
// as PlayQueueActivity doesn't use fragments.
|
// as PlayQueueActivity doesn't use fragments.
|
||||||
|
|
|
@ -633,7 +633,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
final List<VideoStream> sortedVideoStreams = ListHelper
|
final List<VideoStream> sortedVideoStreams = ListHelper
|
||||||
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
||||||
result.getVideoOnlyStreams(), false);
|
result.getVideoOnlyStreams(), false, false);
|
||||||
final int selectedVideoStreamIndex = ListHelper
|
final int selectedVideoStreamIndex = ListHelper
|
||||||
.getDefaultResolutionIndex(this, sortedVideoStreams);
|
.getDefaultResolutionIndex(this, sortedVideoStreams);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.schabi.newpipe.BuildConfig
|
import org.schabi.newpipe.BuildConfig
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
@ -21,30 +20,28 @@ import org.schabi.newpipe.util.ThemeHelper
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Localization.assureCorrectAppLanguage(this)
|
Localization.assureCorrectAppLanguage(this)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
ThemeHelper.setTheme(this)
|
ThemeHelper.setTheme(this)
|
||||||
title = getString(R.string.title_activity_about)
|
title = getString(R.string.title_activity_about)
|
||||||
|
|
||||||
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
setContentView(aboutBinding.root)
|
setContentView(aboutBinding.root)
|
||||||
setSupportActionBar(aboutBinding.aboutToolbar)
|
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
// Create the adapter that will return a fragment for each of the three
|
// Create the adapter that will return a fragment for each of the three
|
||||||
// primary sections of the activity.
|
// primary sections of the activity.
|
||||||
val mAboutStateAdapter = AboutStateAdapter(this)
|
val mAboutStateAdapter = AboutStateAdapter(this)
|
||||||
|
|
||||||
// Set up the ViewPager with the sections adapter.
|
// Set up the ViewPager with the sections adapter.
|
||||||
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
||||||
TabLayoutMediator(
|
TabLayoutMediator(
|
||||||
aboutBinding.aboutTabLayout,
|
aboutBinding.aboutTabLayout,
|
||||||
aboutBinding.aboutViewPager2
|
aboutBinding.aboutViewPager2
|
||||||
) { tab: TabLayout.Tab, position: Int ->
|
) { tab, position ->
|
||||||
when (position) {
|
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
||||||
POS_ABOUT -> tab.setText(R.string.tab_about)
|
|
||||||
POS_LICENSE -> tab.setText(R.string.tab_licenses)
|
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
|
||||||
}
|
|
||||||
}.attach()
|
}.attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,13 +72,14 @@ class AboutActivity : AppCompatActivity() {
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val aboutBinding = FragmentAboutBinding.inflate(inflater, container, false)
|
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
||||||
aboutBinding.aboutAppVersion.text = BuildConfig.VERSION_NAME
|
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
||||||
aboutBinding.aboutGithubLink.openLink(R.string.github_url)
|
aboutGithubLink.openLink(R.string.github_url)
|
||||||
aboutBinding.aboutDonationLink.openLink(R.string.donation_url)
|
aboutDonationLink.openLink(R.string.donation_url)
|
||||||
aboutBinding.aboutWebsiteLink.openLink(R.string.website_url)
|
aboutWebsiteLink.openLink(R.string.website_url)
|
||||||
aboutBinding.aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||||
return aboutBinding.root
|
return root
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,17 +88,29 @@ class AboutActivity : AppCompatActivity() {
|
||||||
* one of the sections/tabs/pages.
|
* one of the sections/tabs/pages.
|
||||||
*/
|
*/
|
||||||
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
|
private val posAbout = 0
|
||||||
|
private val posLicense = 1
|
||||||
|
private val totalCount = 2
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
POS_ABOUT -> AboutFragment()
|
posAbout -> AboutFragment()
|
||||||
POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
// Show 2 total pages.
|
// Show 2 total pages.
|
||||||
return TOTAL_COUNT
|
return totalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageTitle(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> R.string.tab_about
|
||||||
|
posLicense -> R.string.tab_licenses
|
||||||
|
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,10 +127,6 @@ class AboutActivity : AppCompatActivity() {
|
||||||
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
||||||
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
||||||
),
|
),
|
||||||
SoftwareComponent(
|
|
||||||
"CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
|
||||||
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
||||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
||||||
|
@ -191,8 +197,5 @@ class AboutActivity : AppCompatActivity() {
|
||||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
private const val POS_ABOUT = 0
|
|
||||||
private const val POS_LICENSE = 1
|
|
||||||
private const val TOTAL_COUNT = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,60 +87,50 @@ object LicenseFragmentHelper {
|
||||||
return context.getString(color).substring(3)
|
return context.getString(color).substring(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun showLicense(context: Context?, license: License): Disposable {
|
fun showLicense(context: Context?, license: License): Disposable {
|
||||||
|
return showLicense(context, license) { alertDialog ->
|
||||||
|
alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||||
|
return showLicense(context, component.license) { alertDialog ->
|
||||||
|
alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInBrowser(context!!, component.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
context: Context?,
|
||||||
|
license: License,
|
||||||
|
block: (AlertDialog.Builder) -> Unit
|
||||||
|
): Disposable {
|
||||||
return if (context == null) {
|
return if (context == null) {
|
||||||
Disposable.empty()
|
Disposable.empty()
|
||||||
} else {
|
} else {
|
||||||
Observable.fromCallable { getFormattedLicense(context, license) }
|
Observable.fromCallable { getFormattedLicense(context, license) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { formattedLicense: String ->
|
.subscribe { formattedLicense ->
|
||||||
val webViewData = Base64.encodeToString(
|
val webViewData = Base64.encodeToString(
|
||||||
formattedLicense
|
formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING
|
||||||
.toByteArray(StandardCharsets.UTF_8),
|
|
||||||
Base64.NO_PADDING
|
|
||||||
)
|
)
|
||||||
val webView = WebView(context)
|
val webView = WebView(context)
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
val alert = AlertDialog.Builder(context)
|
|
||||||
alert.setTitle(license.name)
|
AlertDialog.Builder(context).apply {
|
||||||
alert.setView(webView)
|
setTitle(license.name)
|
||||||
|
setView(webView)
|
||||||
Localization.assureCorrectAppLanguage(context)
|
Localization.assureCorrectAppLanguage(context)
|
||||||
alert.setNegativeButton(
|
block(this)
|
||||||
context.getString(R.string.ok)
|
show()
|
||||||
) { dialog, _ -> dialog.dismiss() }
|
}
|
||||||
alert.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@JvmStatic
|
|
||||||
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, component.license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense: String ->
|
|
||||||
val webViewData = Base64.encodeToString(
|
|
||||||
formattedLicense
|
|
||||||
.toByteArray(StandardCharsets.UTF_8),
|
|
||||||
Base64.NO_PADDING
|
|
||||||
)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
val alert = AlertDialog.Builder(context)
|
|
||||||
alert.setTitle(component.license.name)
|
|
||||||
alert.setView(webView)
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
alert.setPositiveButton(
|
|
||||||
R.string.dismiss
|
|
||||||
) { dialog, _ -> dialog.dismiss() }
|
|
||||||
alert.setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInBrowser(context, component.link)
|
|
||||||
}
|
|
||||||
alert.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.database.history.dao;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
|
@ -67,6 +68,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
public abstract int deleteStreamHistory(long streamId);
|
public abstract int deleteStreamHistory(long streamId);
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE
|
@Query("SELECT * FROM " + STREAM_TABLE
|
||||||
|
|
||||||
// Select the latest entry and watch count for each stream id on history table
|
// Select the latest entry and watch count for each stream id on history table
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||||
import androidx.room.Transaction;
|
import androidx.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
@ -52,6 +53,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
Flowable<Integer> getMaximumIndexOf(long playlistId);
|
Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
||||||
// get ids of streams of the given playlist
|
// get ids of streams of the given playlist
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Maybe
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
|
@ -31,6 +32,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||||
)
|
)
|
||||||
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM subscriptions s
|
SELECT * FROM subscriptions s
|
||||||
|
@ -47,6 +49,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||||
currentGroupId: Long
|
currentGroupId: Long
|
||||||
): Flowable<List<SubscriptionEntity>>
|
): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM subscriptions s
|
SELECT * FROM subscriptions s
|
||||||
|
|
|
@ -61,6 +61,7 @@ import org.schabi.newpipe.util.FilenameUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||||
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
@ -151,7 +152,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
|
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
|
||||||
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
|
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
|
||||||
.getSortedStreamVideosList(context, info.getVideoStreams(),
|
.getSortedStreamVideosList(context, info.getVideoStreams(),
|
||||||
info.getVideoOnlyStreams(), false));
|
info.getVideoOnlyStreams(), false, false));
|
||||||
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
||||||
|
|
||||||
final DownloadDialog instance = newInstance(info);
|
final DownloadDialog instance = newInstance(info);
|
||||||
|
@ -321,21 +322,15 @@ public class DownloadDialog extends DialogFragment
|
||||||
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||||
dialogBinding.threadsCount.setText(String.valueOf(threads));
|
dialogBinding.threadsCount.setText(String.valueOf(threads));
|
||||||
dialogBinding.threads.setProgress(threads - 1);
|
dialogBinding.threads.setProgress(threads - 1);
|
||||||
dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(final SeekBar seekbar, final int progress,
|
public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress,
|
||||||
final boolean fromUser) {
|
final boolean fromUser) {
|
||||||
final int newProgress = progress + 1;
|
final int newProgress = progress + 1;
|
||||||
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
||||||
.apply();
|
.apply();
|
||||||
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
|
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStartTrackingTouch(final SeekBar p1) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStopTrackingTouch(final SeekBar p1) { }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchStreamsSize();
|
fetchStreamsSize();
|
||||||
|
|
|
@ -29,8 +29,8 @@ public enum UserAction {
|
||||||
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
|
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
|
||||||
PREFERENCES_MIGRATION("migration of preferences"),
|
PREFERENCES_MIGRATION("migration of preferences"),
|
||||||
SHARE_TO_NEWPIPE("share to newpipe"),
|
SHARE_TO_NEWPIPE("share to newpipe"),
|
||||||
CHECK_FOR_NEW_APP_VERSION("check for new app version");
|
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
|
||||||
|
OPEN_INFO_ITEM_DIALOG("open info item dialog");
|
||||||
|
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||||
|
@ -10,7 +11,7 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||||
*/
|
*/
|
||||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||||
@Override
|
@Override
|
||||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
if (dy > 0) {
|
if (dy > 0) {
|
||||||
int pastVisibleItems = 0;
|
int pastVisibleItems = 0;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -46,6 +48,7 @@ class StackItem implements Serializable {
|
||||||
return playQueue;
|
return playQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
||||||
|
|
|
@ -1617,6 +1617,7 @@ public final class VideoDetailFragment
|
||||||
activity,
|
activity,
|
||||||
info.getVideoStreams(),
|
info.getVideoStreams(),
|
||||||
info.getVideoOnlyStreams(),
|
info.getVideoOnlyStreams(),
|
||||||
|
false,
|
||||||
false);
|
false);
|
||||||
selectedVideoStreamIndex = ListHelper
|
selectedVideoStreamIndex = ListHelper
|
||||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||||
|
@ -1994,9 +1995,7 @@ public final class VideoDetailFragment
|
||||||
// Prevent jumping of the player on devices with cutout
|
// Prevent jumping of the player on devices with cutout
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
isMultiWindowOrFullscreen()
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
|
||||||
? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
|
||||||
: WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
|
|
||||||
}
|
}
|
||||||
activity.getWindow().getDecorView().setSystemUiVisibility(0);
|
activity.getWindow().getDecorView().setSystemUiVisibility(0);
|
||||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
|
@ -2018,9 +2017,7 @@ public final class VideoDetailFragment
|
||||||
// Prevent jumping of the player on devices with cutout
|
// Prevent jumping of the player on devices with cutout
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
isMultiWindowOrFullscreen()
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
|
||||||
: WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
|
||||||
}
|
}
|
||||||
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
@ -2037,7 +2034,7 @@ public final class VideoDetailFragment
|
||||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||||
&& isMultiWindowOrFullscreen()) {
|
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
@ -2053,11 +2050,6 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMultiWindowOrFullscreen() {
|
|
||||||
return DeviceUtils.isInMultiWindow(activity)
|
|
||||||
|| (isPlayerAvailable() && player.isFullscreen());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean playerIsNotStopped() {
|
private boolean playerIsNotStopped() {
|
||||||
return isPlayerAvailable() && !player.isStopped();
|
return isPlayerAvailable() && !player.isStopped();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.schabi.newpipe.fragments.list;
|
package org.schabi.newpipe.fragments.list;
|
||||||
|
|
||||||
import android.app.Activity;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
@ -17,37 +19,26 @@ import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewbinding.ViewBinding;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
|
||||||
import org.schabi.newpipe.views.SuperScrollLayoutManager;
|
import org.schabi.newpipe.views.SuperScrollLayoutManager;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
|
||||||
|
|
||||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||||
|
@ -79,11 +70,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetach() {
|
|
||||||
super.onDetach();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -220,14 +206,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ViewBinding getListFooter() {
|
|
||||||
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||||
return new SuperScrollLayoutManager(activity);
|
return new SuperScrollLayoutManager(activity);
|
||||||
}
|
}
|
||||||
|
@ -252,11 +234,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||||
|
|
||||||
infoListAdapter.setUseGridVariant(useGrid);
|
infoListAdapter.setUseGridVariant(useGrid);
|
||||||
infoListAdapter.setFooter(getListFooter().getRoot());
|
|
||||||
|
|
||||||
final ViewBinding listHeader = getListHeader();
|
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
|
||||||
if (listHeader != null) {
|
if (listHeaderSupplier != null) {
|
||||||
infoListAdapter.setHeader(listHeader.getRoot());
|
infoListAdapter.setHeaderSupplier(listHeaderSupplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsList.setAdapter(infoListAdapter);
|
itemsList.setAdapter(infoListAdapter);
|
||||||
|
@ -271,7 +252,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final StreamInfoItem selectedItem) {
|
public void selected(final StreamInfoItem selectedItem) {
|
||||||
onStreamSelected(selectedItem);
|
onStreamSelected(selectedItem);
|
||||||
|
@ -279,11 +260,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void held(final StreamInfoItem selectedItem) {
|
public void held(final StreamInfoItem selectedItem) {
|
||||||
showStreamDialog(selectedItem);
|
showInfoItemDialog(selectedItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final ChannelInfoItem selectedItem) {
|
public void selected(final ChannelInfoItem selectedItem) {
|
||||||
try {
|
try {
|
||||||
|
@ -299,7 +280,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final PlaylistInfoItem selectedItem) {
|
public void selected(final PlaylistInfoItem selectedItem) {
|
||||||
try {
|
try {
|
||||||
|
@ -315,20 +296,97 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final CommentsInfoItem selectedItem) {
|
public void selected(final CommentsInfoItem selectedItem) {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure that there is always a scroll listener (e.g. when rotating the device)
|
||||||
|
useNormalItemListScrollListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all listeners and adds the normal scroll listener to the {@link #itemsList}.
|
||||||
|
*/
|
||||||
|
protected void useNormalItemListScrollListener() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "useNormalItemListScrollListener called");
|
||||||
|
}
|
||||||
itemsList.clearOnScrollListeners();
|
itemsList.clearOnScrollListeners();
|
||||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all listeners and adds the initial scroll listener to the {@link #itemsList}.
|
||||||
|
* <br/>
|
||||||
|
* Which tries to load more items when not enough are in the view (not scrollable)
|
||||||
|
* and more are available.
|
||||||
|
* <br/>
|
||||||
|
* Note: This method only works because "This callback will also be called if visible
|
||||||
|
* item range changes after a layout calculation. In that case, dx and dy will be 0."
|
||||||
|
* - which might be unexpected because no actual scrolling occurs...
|
||||||
|
* <br/>
|
||||||
|
* This listener will be replaced by DefaultItemListOnScrolledDownListener when
|
||||||
|
* <ul>
|
||||||
|
* <li>the view was actually scrolled</li>
|
||||||
|
* <li>the view is scrollable</li>
|
||||||
|
* <li>no more items can be loaded</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
protected void useInitialItemListLoadScrollListener() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "useInitialItemListLoadScrollListener called");
|
||||||
|
}
|
||||||
|
itemsList.clearOnScrollListeners();
|
||||||
|
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull final RecyclerView recyclerView,
|
||||||
|
final int dx, final int dy) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
|
if (dy != 0) {
|
||||||
|
log("Vertical scroll occurred");
|
||||||
|
|
||||||
|
useNormalItemListScrollListener();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLoading.get()) {
|
||||||
|
log("Still loading data -> Skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasMoreItems()) {
|
||||||
|
log("No more items to load");
|
||||||
|
|
||||||
|
useNormalItemListScrollListener();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (itemsList.canScrollVertically(1)
|
||||||
|
|| itemsList.canScrollVertically(-1)) {
|
||||||
|
log("View is scrollable");
|
||||||
|
|
||||||
|
useNormalItemListScrollListener();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Loading more data");
|
||||||
|
loadMoreItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(final String msg) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "initItemListLoadScrollListener - " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener {
|
||||||
@Override
|
@Override
|
||||||
public void onScrolledDown(final RecyclerView recyclerView) {
|
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||||
onScrollToBottom();
|
onScrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
||||||
|
@ -344,55 +402,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void showStreamDialog(final StreamInfoItem item) {
|
protected void showInfoItemDialog(final StreamInfoItem item) {
|
||||||
final Context context = getContext();
|
try {
|
||||||
final Activity activity = getActivity();
|
new InfoItemDialog.Builder(getActivity(), getContext(), this, item).create().show();
|
||||||
if (context == null || context.getResources() == null || activity == null) {
|
} catch (final IllegalArgumentException e) {
|
||||||
return;
|
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
|
||||||
}
|
}
|
||||||
final List<StreamDialogEntry> entries = new ArrayList<>();
|
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
|
||||||
entries.add(StreamDialogEntry.enqueue);
|
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
|
||||||
entries.add(StreamDialogEntry.enqueue_next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.start_here_on_popup,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
}
|
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
|
||||||
|
|
||||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
|
||||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -418,6 +433,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
// Load and handle
|
// Load and handle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startLoading(final boolean forceLoad) {
|
||||||
|
useInitialItemListLoadScrollListener();
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract void loadMoreItems();
|
protected abstract void loadMoreItems();
|
||||||
|
|
||||||
protected abstract boolean hasMoreItems();
|
protected abstract boolean hasMoreItems();
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
@ -27,8 +28,8 @@ import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInfo<I>>
|
||||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
extends BaseListFragment<L, ListExtractor.InfoItemsPage<I>> {
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@State
|
@State
|
||||||
|
@ -37,7 +38,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
protected String url;
|
protected String url;
|
||||||
|
|
||||||
private final UserAction errorUserAction;
|
private final UserAction errorUserAction;
|
||||||
protected I currentInfo;
|
protected L currentInfo;
|
||||||
protected Page currentNextPage;
|
protected Page currentNextPage;
|
||||||
protected Disposable currentWorker;
|
protected Disposable currentWorker;
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
super.onResume();
|
super.onResume();
|
||||||
// Check if it was loading when the fragment was stopped/paused,
|
// Check if it was loading when the fragment was stopped/paused,
|
||||||
if (wasLoading.getAndSet(false)) {
|
if (wasLoading.getAndSet(false)) {
|
||||||
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
|
if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) {
|
||||||
loadMoreItems();
|
loadMoreItems();
|
||||||
} else {
|
} else {
|
||||||
doInitialLoadLogic();
|
doInitialLoadLogic();
|
||||||
|
@ -97,7 +98,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
super.readFrom(savedObjects);
|
super.readFrom(savedObjects);
|
||||||
currentInfo = (I) savedObjects.poll();
|
currentInfo = (L) savedObjects.poll();
|
||||||
currentNextPage = (Page) savedObjects.poll();
|
currentNextPage = (Page) savedObjects.poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +106,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
// Load and handle
|
// Load and handle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void doInitialLoadLogic() {
|
protected void doInitialLoadLogic() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "doInitialLoadLogic() called");
|
Log.d(TAG, "doInitialLoadLogic() called");
|
||||||
|
@ -123,7 +125,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
* @param forceLoad allow or disallow the result to come from the cache
|
* @param forceLoad allow or disallow the result to come from the cache
|
||||||
* @return Rx {@link Single} containing the {@link ListInfo}
|
* @return Rx {@link Single} containing the {@link ListInfo}
|
||||||
*/
|
*/
|
||||||
protected abstract Single<I> loadResult(boolean forceLoad);
|
protected abstract Single<L> loadResult(boolean forceLoad);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
|
@ -139,7 +141,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
currentWorker = loadResult(forceLoad)
|
currentWorker = loadResult(forceLoad)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@NonNull I result) -> {
|
.subscribe((@NonNull L result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
currentNextPage = result.getNextPage();
|
currentNextPage = result.getNextPage();
|
||||||
|
@ -156,8 +158,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
*
|
*
|
||||||
* @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage}
|
* @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage}
|
||||||
*/
|
*/
|
||||||
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
protected abstract Single<ListExtractor.InfoItemsPage<I>> loadMoreItemsLogic();
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void loadMoreItems() {
|
protected void loadMoreItems() {
|
||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
|
|
||||||
|
@ -171,9 +174,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doFinally(this::allowDownwardFocusScroll)
|
.doFinally(this::allowDownwardFocusScroll)
|
||||||
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
.subscribe(infoItemsPage -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
handleNextItems(InfoItemsPage);
|
handleNextItems(infoItemsPage);
|
||||||
}, (@NonNull Throwable throwable) ->
|
}, (@NonNull Throwable throwable) ->
|
||||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
||||||
errorUserAction, "Loading more items: " + url, serviceId)));
|
errorUserAction, "Loading more items: " + url, serviceId)));
|
||||||
|
@ -192,7 +195,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage<I> result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
currentNextPage = result.getNextPage();
|
currentNextPage = result.getNextPage();
|
||||||
|
@ -216,14 +219,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final I result) {
|
public void handleResult(@NonNull final L result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
name = result.getName();
|
name = result.getName();
|
||||||
setTitle(name);
|
setTitle(name);
|
||||||
|
|
||||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||||
if (result.getRelatedItems().size() > 0) {
|
if (!result.getRelatedItems().isEmpty()) {
|
||||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,7 +243,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||||
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);
|
errors.removeIf(ContentNotSupportedException.class::isInstance);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
||||||
|
|
|
@ -22,7 +22,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.viewbinding.ViewBinding;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.jakewharton.rxbinding4.view.RxView;
|
import com.jakewharton.rxbinding4.view.RxView;
|
||||||
|
@ -36,7 +35,6 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
|
@ -51,13 +49,14 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
@ -69,7 +68,7 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
import io.reactivex.rxjava3.functions.Function;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
|
||||||
implements View.OnClickListener {
|
implements View.OnClickListener {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
|
@ -150,12 +149,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
headerBinding = ChannelHeaderBinding
|
headerBinding = ChannelHeaderBinding
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
playlistControlBinding = headerBinding.playlistControl;
|
playlistControlBinding = headerBinding.playlistControl;
|
||||||
|
|
||||||
return headerBinding;
|
return headerBinding::getRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -189,13 +188,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openRssFeed() {
|
|
||||||
final ChannelInfo info = currentInfo;
|
|
||||||
if (info != null) {
|
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
@ -208,7 +200,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
setNotify(value);
|
setNotify(value);
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_rss:
|
case R.id.menu_item_rss:
|
||||||
openRssFeed();
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.openUrlInBrowser(
|
||||||
|
requireContext(), currentInfo.getFeedUrl(), false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_openInBrowser:
|
case R.id.menu_item_openInBrowser:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
|
@ -438,7 +433,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,12 +570,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue(final int index) {
|
private PlayQueue getPlayQueue(final int index) {
|
||||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||||
for (final InfoItem i : infoListAdapter.getItemsList()) {
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
if (i instanceof StreamInfoItem) {
|
.map(StreamInfoItem.class::cast)
|
||||||
streamItems.add((StreamInfoItem) i);
|
.collect(Collectors.toList());
|
||||||
}
|
|
||||||
}
|
|
||||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||||
currentInfo.getNextPage(), streamItems, index);
|
currentInfo.getNextPage(), streamItems, index);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -22,7 +23,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private TextView emptyStateDesc;
|
private TextView emptyStateDesc;
|
||||||
|
@ -67,7 +68,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
|
@ -53,7 +54,7 @@ import io.reactivex.rxjava3.core.Single;
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
public class KioskFragment extends BaseListInfoFragment<StreamInfoItem, KioskInfo> {
|
||||||
@State
|
@State
|
||||||
String kioskId = "";
|
String kioskId = "";
|
||||||
String kioskTranslatedName;
|
String kioskTranslatedName;
|
||||||
|
@ -145,7 +146,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
public Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
|
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.schabi.newpipe.fragments.list.playlist;
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
import android.app.Activity;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -15,7 +18,10 @@ import android.view.ViewGroup;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.viewbinding.ViewBinding;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.shape.CornerFamily;
|
||||||
|
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
|
@ -33,26 +39,23 @@ import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
@ -60,11 +63,7 @@ import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> {
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
|
||||||
|
|
||||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|
||||||
|
|
||||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||||
|
|
||||||
|
@ -120,12 +119,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
headerBinding = PlaylistHeaderBinding
|
headerBinding = PlaylistHeaderBinding
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
playlistControlBinding = headerBinding.playlistControl;
|
playlistControlBinding = headerBinding.playlistControl;
|
||||||
|
|
||||||
return headerBinding;
|
return headerBinding::getRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -140,60 +139,22 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void showStreamDialog(final StreamInfoItem item) {
|
protected void showInfoItemDialog(final StreamInfoItem item) {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
final Activity activity = getActivity();
|
try {
|
||||||
if (context == null || context.getResources() == null || activity == null) {
|
final InfoItemDialog.Builder dialogBuilder =
|
||||||
return;
|
new InfoItemDialog.Builder(getActivity(), context, this, item);
|
||||||
|
|
||||||
|
dialogBuilder
|
||||||
|
.setAction(
|
||||||
|
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
|
||||||
|
(f, infoItem) -> NavigationHelper.playOnBackgroundPlayer(
|
||||||
|
context, getPlayQueueStartingAt(infoItem), true))
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
|
||||||
entries.add(StreamDialogEntry.enqueue);
|
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
|
||||||
entries.add(StreamDialogEntry.enqueue_next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.start_here_on_popup,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
}
|
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
|
||||||
|
|
||||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(context,
|
|
||||||
getPlayQueueStartingAt(infoItem), true));
|
|
||||||
|
|
||||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
|
||||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -249,7 +210,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
|
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,9 +289,14 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
||||||
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
||||||
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
|
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
|
||||||
headerBinding.uploaderAvatarView.setDisableCircularTransformation(true);
|
final ShapeAppearanceModel model = ShapeAppearanceModel.builder()
|
||||||
headerBinding.uploaderAvatarView.setBorderColor(
|
.setAllCorners(CornerFamily.ROUNDED, 0f)
|
||||||
getResources().getColor(R.color.transparent_background_color));
|
.build(); // this turns the image back into a square
|
||||||
|
headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
|
||||||
|
headerBinding.uploaderAvatarView.setStrokeColor(
|
||||||
|
ColorStateList.valueOf(ContextCompat.getColor(
|
||||||
|
requireContext(), R.color.transparent_background_color))
|
||||||
|
);
|
||||||
headerBinding.uploaderAvatarView.setImageDrawable(
|
headerBinding.uploaderAvatarView.setImageDrawable(
|
||||||
AppCompatResources.getDrawable(requireContext(),
|
AppCompatResources.getDrawable(requireContext(),
|
||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
|
@ -413,7 +379,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
if (bookmarkReactor != null) {
|
if (bookmarkReactor != null) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -34,8 +35,10 @@ public class SuggestionListAdapter
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
||||||
|
final int viewType) {
|
||||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||||
.inflate(R.layout.item_search_suggestion, parent, false));
|
.inflate(R.layout.item_search_suggestion, parent, false));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.schabi.newpipe.fragments.list.videos;
|
package org.schabi.newpipe.fragments.list.videos;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
@ -12,11 +11,11 @@ import android.view.ViewGroup;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.viewbinding.ViewBinding;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
@ -24,14 +23,14 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.util.RelatedItemInfo;
|
import org.schabi.newpipe.util.RelatedItemInfo;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String INFO_KEY = "related_info_key";
|
private static final String INFO_KEY = "related_info_key";
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
private RelatedItemInfo relatedItemInfo;
|
private RelatedItemInfo relatedItemInfo;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -54,11 +53,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(@NonNull final Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
|
@ -66,12 +60,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
headerBinding = null;
|
headerBinding = null;
|
||||||
|
@ -79,8 +67,11 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != null) {
|
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
headerBinding = RelatedItemsHeaderBinding
|
headerBinding = RelatedItemsHeaderBinding
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
|
|
||||||
|
@ -91,14 +82,12 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
||||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
||||||
return headerBinding;
|
|
||||||
} else {
|
return headerBinding::getRoot;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
|
||||||
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
|
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +117,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
}
|
}
|
||||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
||||||
|
|
||||||
disposables.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -137,11 +125,13 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTitle(final String title) {
|
public void setTitle(final String title) {
|
||||||
|
// Nothing to do - override parent
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
@NonNull final MenuInflater inflater) {
|
@NonNull final MenuInflater inflater) {
|
||||||
|
// Nothing to do - override parent
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInitialData(final StreamInfo info) {
|
private void setInitialData(final StreamInfo info) {
|
||||||
|
@ -169,11 +159,10 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||||
final String s) {
|
final String s) {
|
||||||
final SharedPreferences pref =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
|
||||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
|
||||||
if (headerBinding != null) {
|
if (headerBinding != null) {
|
||||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
headerBinding.autoplaySwitch.setChecked(
|
||||||
|
sharedPreferences.getBoolean(
|
||||||
|
getString(R.string.auto_queue_key), false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
package org.schabi.newpipe.info_list;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
|
|
||||||
public class InfoItemDialog {
|
|
||||||
private final AlertDialog dialog;
|
|
||||||
|
|
||||||
public InfoItemDialog(@NonNull final Activity activity,
|
|
||||||
@NonNull final StreamInfoItem info,
|
|
||||||
@NonNull final String[] commands,
|
|
||||||
@NonNull final DialogInterface.OnClickListener actions) {
|
|
||||||
this(activity, commands, actions, info.getName(), info.getUploaderName());
|
|
||||||
}
|
|
||||||
|
|
||||||
public InfoItemDialog(@NonNull final Activity activity,
|
|
||||||
@NonNull final String[] commands,
|
|
||||||
@NonNull final DialogInterface.OnClickListener actions,
|
|
||||||
@NonNull final String title,
|
|
||||||
@Nullable final String additionalDetail) {
|
|
||||||
|
|
||||||
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
|
|
||||||
bannerView.setSelected(true);
|
|
||||||
|
|
||||||
final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
|
||||||
titleView.setText(title);
|
|
||||||
|
|
||||||
final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
|
|
||||||
if (additionalDetail != null) {
|
|
||||||
detailsView.setText(additionalDetail);
|
|
||||||
detailsView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
detailsView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog = new AlertDialog.Builder(activity)
|
|
||||||
.setCustomTitle(bannerView)
|
|
||||||
.setItems(commands, actions)
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
@ -34,6 +35,7 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
|
@ -74,18 +76,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||||
|
|
||||||
|
private final LayoutInflater layoutInflater;
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
private final ArrayList<InfoItem> infoItemList;
|
private final List<InfoItem> infoItemList;
|
||||||
private final HistoryRecordManager recordManager;
|
private final HistoryRecordManager recordManager;
|
||||||
|
|
||||||
private boolean useMiniVariant = false;
|
private boolean useMiniVariant = false;
|
||||||
private boolean useGridVariant = false;
|
private boolean useGridVariant = false;
|
||||||
private boolean showFooter = false;
|
private boolean showFooter = false;
|
||||||
private View header = null;
|
|
||||||
private View footer = null;
|
private Supplier<View> headerSupplier = null;
|
||||||
|
|
||||||
public InfoListAdapter(final Context context) {
|
public InfoListAdapter(final Context context) {
|
||||||
this.recordManager = new HistoryRecordManager(context);
|
layoutInflater = LayoutInflater.from(context);
|
||||||
|
recordManager = new HistoryRecordManager(context);
|
||||||
infoItemBuilder = new InfoItemBuilder(context);
|
infoItemBuilder = new InfoItemBuilder(context);
|
||||||
infoItemList = new ArrayList<>();
|
infoItemList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
@ -129,12 +133,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
|
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
|
||||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||||
+ "header = " + header + ", footer = " + footer + ", "
|
+ "hasHeader = " + hasHeader() + ", "
|
||||||
+ "showFooter = " + showFooter);
|
+ "showFooter = " + showFooter);
|
||||||
}
|
}
|
||||||
notifyItemRangeInserted(offsetStart, data.size());
|
notifyItemRangeInserted(offsetStart, data.size());
|
||||||
|
|
||||||
if (footer != null && showFooter) {
|
if (showFooter) {
|
||||||
final int footerNow = sizeConsideringHeaderOffset();
|
final int footerNow = sizeConsideringHeaderOffset();
|
||||||
notifyItemMoved(offsetStart, footerNow);
|
notifyItemMoved(offsetStart, footerNow);
|
||||||
|
|
||||||
|
@ -145,43 +149,6 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInfoItemList(final List<? extends InfoItem> data) {
|
|
||||||
infoItemList.clear();
|
|
||||||
infoItemList.addAll(data);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addInfoItem(@Nullable final InfoItem data) {
|
|
||||||
if (data == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
|
|
||||||
+ infoItemList.size() + ", thread = " + Thread.currentThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
final int positionInserted = sizeConsideringHeaderOffset();
|
|
||||||
infoItemList.add(data);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
|
|
||||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
|
||||||
+ "header = " + header + ", footer = " + footer + ", "
|
|
||||||
+ "showFooter = " + showFooter);
|
|
||||||
}
|
|
||||||
notifyItemInserted(positionInserted);
|
|
||||||
|
|
||||||
if (footer != null && showFooter) {
|
|
||||||
final int footerNow = sizeConsideringHeaderOffset();
|
|
||||||
notifyItemMoved(positionInserted, footerNow);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "addInfoItem() footer from " + positionInserted
|
|
||||||
+ " to " + footerNow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearStreamItemList() {
|
public void clearStreamItemList() {
|
||||||
if (infoItemList.isEmpty()) {
|
if (infoItemList.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
@ -190,16 +157,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeader(final View header) {
|
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
|
||||||
final boolean changed = header != this.header;
|
final boolean changed = headerSupplier != this.headerSupplier;
|
||||||
this.header = header;
|
this.headerSupplier = headerSupplier;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFooter(final View view) {
|
protected boolean hasHeader() {
|
||||||
this.footer = view;
|
return this.headerSupplier != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showFooter(final boolean show) {
|
public void showFooter(final boolean show) {
|
||||||
|
@ -219,48 +186,49 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
}
|
}
|
||||||
|
|
||||||
private int sizeConsideringHeaderOffset() {
|
private int sizeConsideringHeaderOffset() {
|
||||||
final int i = infoItemList.size() + (header != null ? 1 : 0);
|
final int i = infoItemList.size() + (hasHeader() ? 1 : 0);
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
||||||
}
|
}
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<InfoItem> getItemsList() {
|
public List<InfoItem> getItemsList() {
|
||||||
return infoItemList;
|
return infoItemList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
int count = infoItemList.size();
|
int count = infoItemList.size();
|
||||||
if (header != null) {
|
if (hasHeader()) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
if (footer != null && showFooter) {
|
if (showFooter) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "getItemCount() called with: "
|
Log.d(TAG, "getItemCount() called with: "
|
||||||
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
|
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
|
||||||
+ "header = " + header + ", footer = " + footer + ", "
|
+ "hasHeader = " + hasHeader() + ", "
|
||||||
+ "showFooter = " + showFooter);
|
+ "showFooter = " + showFooter);
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("FinalParameters")
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header != null && position == 0) {
|
if (hasHeader() && position == 0) {
|
||||||
return HEADER_TYPE;
|
return HEADER_TYPE;
|
||||||
} else if (header != null) {
|
} else if (hasHeader()) {
|
||||||
position--;
|
position--;
|
||||||
}
|
}
|
||||||
if (footer != null && position == infoItemList.size() && showFooter) {
|
if (position == infoItemList.size() && showFooter) {
|
||||||
return FOOTER_TYPE;
|
return FOOTER_TYPE;
|
||||||
}
|
}
|
||||||
final InfoItem item = infoItemList.get(position);
|
final InfoItem item = infoItemList.get(position);
|
||||||
|
@ -290,10 +258,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||||
}
|
}
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
// #4475 and #3368
|
||||||
|
// Always create a new instance otherwise the same instance
|
||||||
|
// is sometimes reused which causes a crash
|
||||||
case HEADER_TYPE:
|
case HEADER_TYPE:
|
||||||
return new HFHolder(header);
|
return new HFHolder(headerSupplier.get());
|
||||||
case FOOTER_TYPE:
|
case FOOTER_TYPE:
|
||||||
return new HFHolder(footer);
|
return new HFHolder(PignateFooterBinding
|
||||||
|
.inflate(layoutInflater, parent, false)
|
||||||
|
.getRoot()
|
||||||
|
);
|
||||||
case MINI_STREAM_HOLDER_TYPE:
|
case MINI_STREAM_HOLDER_TYPE:
|
||||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case STREAM_HOLDER_TYPE:
|
case STREAM_HOLDER_TYPE:
|
||||||
|
@ -322,42 +296,17 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||||
|
final int position) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onBindViewHolder() called with: "
|
Log.d(TAG, "onBindViewHolder() called with: "
|
||||||
+ "holder = [" + holder.getClass().getSimpleName() + "], "
|
+ "holder = [" + holder.getClass().getSimpleName() + "], "
|
||||||
+ "position = [" + position + "]");
|
+ "position = [" + position + "]");
|
||||||
}
|
}
|
||||||
if (holder instanceof InfoItemHolder) {
|
if (holder instanceof InfoItemHolder) {
|
||||||
// If header isn't null, offset the items by -1
|
((InfoItemHolder) holder).updateFromItem(
|
||||||
if (header != null) {
|
// If header is present, offset the items by -1
|
||||||
position--;
|
infoItemList.get(hasHeader() ? position - 1 : position), recordManager);
|
||||||
}
|
|
||||||
|
|
||||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
|
|
||||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
|
||||||
((HFHolder) holder).view = header;
|
|
||||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
|
|
||||||
&& footer != null && showFooter) {
|
|
||||||
((HFHolder) holder).view = footer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
|
|
||||||
@NonNull final List<Object> payloads) {
|
|
||||||
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
|
||||||
for (final Object payload : payloads) {
|
|
||||||
if (payload instanceof StreamStateEntity) {
|
|
||||||
((InfoItemHolder) holder).updateState(infoItemList
|
|
||||||
.get(header == null ? position : position - 1), recordManager);
|
|
||||||
} else if (payload instanceof Boolean) {
|
|
||||||
((InfoItemHolder) holder).updateState(infoItemList
|
|
||||||
.get(header == null ? position : position - 1), recordManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onBindViewHolder(holder, position);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,12 +320,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HFHolder extends RecyclerView.ViewHolder {
|
static class HFHolder extends RecyclerView.ViewHolder {
|
||||||
public View view;
|
|
||||||
|
|
||||||
HFHolder(final View v) {
|
HFHolder(final View v) {
|
||||||
super(v);
|
super(v);
|
||||||
view = v;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,356 @@
|
||||||
|
package org.schabi.newpipe.info_list.dialog;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for a {@link StreamInfoItem}.
|
||||||
|
* The dialog's content are actions that can be performed on the {@link StreamInfoItem}.
|
||||||
|
* This dialog is mostly used for longpress context menus.
|
||||||
|
*/
|
||||||
|
public final class InfoItemDialog {
|
||||||
|
private static final String TAG = Build.class.getSimpleName();
|
||||||
|
/**
|
||||||
|
* Ideally, {@link InfoItemDialog} would extend {@link AlertDialog}.
|
||||||
|
* However, extending {@link AlertDialog} requires many additional lines
|
||||||
|
* and brings more complexity to this class, especially the constructor.
|
||||||
|
* To circumvent this, an {@link AlertDialog.Builder} is used in the constructor.
|
||||||
|
* Its result is stored in this class variable to allow access via the {@link #show()} method.
|
||||||
|
*/
|
||||||
|
private final AlertDialog dialog;
|
||||||
|
|
||||||
|
private InfoItemDialog(@NonNull final Activity activity,
|
||||||
|
@NonNull final Fragment fragment,
|
||||||
|
@NonNull final StreamInfoItem info,
|
||||||
|
@NonNull final List<StreamDialogEntry> entries) {
|
||||||
|
|
||||||
|
// Create the dialog's title
|
||||||
|
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
|
||||||
|
bannerView.setSelected(true);
|
||||||
|
|
||||||
|
final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
||||||
|
titleView.setText(info.getName());
|
||||||
|
|
||||||
|
final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
|
||||||
|
if (info.getUploaderName() != null) {
|
||||||
|
detailsView.setText(info.getUploaderName());
|
||||||
|
detailsView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
detailsView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the entry's descriptions which are displayed in the dialog
|
||||||
|
final String[] items = entries.stream()
|
||||||
|
.map(entry -> entry.getString(activity)).toArray(String[]::new);
|
||||||
|
|
||||||
|
// Call an entry's action / onClick method when the entry is selected.
|
||||||
|
final DialogInterface.OnClickListener action = (d, index) ->
|
||||||
|
entries.get(index).action.onClick(fragment, info);
|
||||||
|
|
||||||
|
dialog = new AlertDialog.Builder(activity)
|
||||||
|
.setCustomTitle(bannerView)
|
||||||
|
.setItems(items, action)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Builder to generate a {@link InfoItemDialog} for a {@link StreamInfoItem}.</p>
|
||||||
|
* Use {@link #addEntry(StreamDialogDefaultEntry)}
|
||||||
|
* and {@link #addAllEntries(StreamDialogDefaultEntry...)} to add options to the dialog.
|
||||||
|
* <br>
|
||||||
|
* Custom actions for entries can be set using
|
||||||
|
* {@link #setAction(StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}.
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
@NonNull private final Activity activity;
|
||||||
|
@NonNull private final Context context;
|
||||||
|
@NonNull private final StreamInfoItem infoItem;
|
||||||
|
@NonNull private final Fragment fragment;
|
||||||
|
@NonNull private final List<StreamDialogEntry> entries = new ArrayList<>();
|
||||||
|
private final boolean addDefaultEntriesAutomatically;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a {@link Builder builder} instance for a {@link StreamInfoItem}
|
||||||
|
* that automatically adds the some default entries
|
||||||
|
* at the top and bottom of the dialog.</p>
|
||||||
|
* The dialog has the following structure:
|
||||||
|
* <pre>
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* | ENQUEUE |
|
||||||
|
* | ENQUEUE_NEXT |
|
||||||
|
* | START_ON_BACKGROUND |
|
||||||
|
* | START_ON_POPUP |
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* | entries added manually with |
|
||||||
|
* | addEntry() and addAllEntries() |
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* | APPEND_PLAYLIST |
|
||||||
|
* | SHARE |
|
||||||
|
* | OPEN_IN_BROWSER |
|
||||||
|
* | PLAY_WITH_KODI |
|
||||||
|
* | MARK_AS_WATCHED |
|
||||||
|
* | SHOW_CHANNEL_DETAILS |
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* </pre>
|
||||||
|
* Please note that some entries are not added depending on the user's preferences,
|
||||||
|
* the item's {@link StreamType} and the current player state.
|
||||||
|
*
|
||||||
|
* @param activity
|
||||||
|
* @param context
|
||||||
|
* @param fragment
|
||||||
|
* @param infoItem the item for this dialog; all entries and their actions work with
|
||||||
|
* this {@link StreamInfoItem}
|
||||||
|
* @throws IllegalArgumentException if <code>activity, context</code>
|
||||||
|
* or resources is <code>null</code>
|
||||||
|
*/
|
||||||
|
public Builder(final Activity activity,
|
||||||
|
final Context context,
|
||||||
|
@NonNull final Fragment fragment,
|
||||||
|
@NonNull final StreamInfoItem infoItem) {
|
||||||
|
this(activity, context, fragment, infoItem, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create an instance of this {@link Builder} for a {@link StreamInfoItem}.</p>
|
||||||
|
* <p>If {@code addDefaultEntriesAutomatically} is set to {@code true},
|
||||||
|
* some default entries are added to the top and bottom of the dialog.</p>
|
||||||
|
* The dialog has the following structure:
|
||||||
|
* <pre>
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* | ENQUEUE |
|
||||||
|
* | ENQUEUE_NEXT |
|
||||||
|
* | START_ON_BACKGROUND |
|
||||||
|
* | START_ON_POPUP |
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* | entries added manually with |
|
||||||
|
* | addEntry() and addAllEntries() |
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* | APPEND_PLAYLIST |
|
||||||
|
* | SHARE |
|
||||||
|
* | OPEN_IN_BROWSER |
|
||||||
|
* | PLAY_WITH_KODI |
|
||||||
|
* | MARK_AS_WATCHED |
|
||||||
|
* | SHOW_CHANNEL_DETAILS |
|
||||||
|
* + - - - - - - - - - - - - - - - - - - - - - -+
|
||||||
|
* </pre>
|
||||||
|
* Please note that some entries are not added depending on the user's preferences,
|
||||||
|
* the item's {@link StreamType} and the current player state.
|
||||||
|
*
|
||||||
|
* @param activity
|
||||||
|
* @param context
|
||||||
|
* @param fragment
|
||||||
|
* @param infoItem
|
||||||
|
* @param addDefaultEntriesAutomatically
|
||||||
|
* whether default entries added with {@link #addDefaultBeginningEntries()}
|
||||||
|
* and {@link #addDefaultEndEntries()} are added automatically when generating
|
||||||
|
* the {@link InfoItemDialog}.
|
||||||
|
* <br/>
|
||||||
|
* Entries added with {@link #addEntry(StreamDialogDefaultEntry)} and
|
||||||
|
* {@link #addAllEntries(StreamDialogDefaultEntry...)} are added in between.
|
||||||
|
* @throws IllegalArgumentException if <code>activity, context</code>
|
||||||
|
* or resources is <code>null</code>
|
||||||
|
*/
|
||||||
|
public Builder(final Activity activity,
|
||||||
|
final Context context,
|
||||||
|
@NonNull final Fragment fragment,
|
||||||
|
@NonNull final StreamInfoItem infoItem,
|
||||||
|
final boolean addDefaultEntriesAutomatically) {
|
||||||
|
if (activity == null || context == null || context.getResources() == null) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "activity, context or resources is null: activity = "
|
||||||
|
+ activity + ", context = " + context);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("activity, context or resources is null");
|
||||||
|
}
|
||||||
|
this.activity = activity;
|
||||||
|
this.context = context;
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.infoItem = infoItem;
|
||||||
|
this.addDefaultEntriesAutomatically = addDefaultEntriesAutomatically;
|
||||||
|
if (addDefaultEntriesAutomatically) {
|
||||||
|
addDefaultBeginningEntries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new entry and appends it to the current entry list.
|
||||||
|
* @param entry the entry to add
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addEntry(@NonNull final StreamDialogDefaultEntry entry) {
|
||||||
|
entries.add(entry.toStreamDialogEntry());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds new entries. These are appended to the current entry list.
|
||||||
|
* @param newEntries the entries to add
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addAllEntries(@NonNull final StreamDialogDefaultEntry... newEntries) {
|
||||||
|
Stream.of(newEntries).forEach(this::addEntry);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Change an entries' action that is called when the entry is selected.</p>
|
||||||
|
* <p><strong>Warning:</strong> Only use this method when the entry has been already added.
|
||||||
|
* Changing the action of an entry which has not been added to the Builder yet
|
||||||
|
* does not have an effect.</p>
|
||||||
|
* @param entry the entry to change
|
||||||
|
* @param action the action to perform when the entry is selected
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder setAction(@NonNull final StreamDialogDefaultEntry entry,
|
||||||
|
@NonNull final StreamDialogEntry.StreamDialogEntryAction action) {
|
||||||
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
|
if (entries.get(i).resource == entry.resource) {
|
||||||
|
entries.set(i, new StreamDialogEntry(entry.resource, action));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds {@link StreamDialogDefaultEntry#ENQUEUE} if the player is open and
|
||||||
|
* {@link StreamDialogDefaultEntry#ENQUEUE_NEXT} if there are multiple streams
|
||||||
|
* in the play queue.
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addEnqueueEntriesIfNeeded() {
|
||||||
|
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||||
|
addEntry(StreamDialogDefaultEntry.ENQUEUE);
|
||||||
|
|
||||||
|
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||||
|
addEntry(StreamDialogDefaultEntry.ENQUEUE_NEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the {@link StreamDialogDefaultEntry#START_HERE_ON_BACKGROUND}.
|
||||||
|
* If the {@link #infoItem} is not a pure audio (live) stream,
|
||||||
|
* {@link StreamDialogDefaultEntry#START_HERE_ON_POPUP} is added, too.
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addStartHereEntries() {
|
||||||
|
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
|
||||||
|
if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
|
||||||
|
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
||||||
|
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds {@link StreamDialogDefaultEntry#MARK_AS_WATCHED} if the watch history is enabled
|
||||||
|
* and the stream is not a livestream.
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addMarkAsWatchedEntryIfNeeded() {
|
||||||
|
final boolean isWatchHistoryEnabled = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
||||||
|
if (isWatchHistoryEnabled
|
||||||
|
&& infoItem.getStreamType() != StreamType.LIVE_STREAM
|
||||||
|
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
||||||
|
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the {@link StreamDialogDefaultEntry#PLAY_WITH_KODI} entry if it is needed.
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addPlayWithKodiEntryIfNeeded() {
|
||||||
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
|
addEntry(StreamDialogDefaultEntry.PLAY_WITH_KODI);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the entries which are usually at the top of the action list.
|
||||||
|
* <br/>
|
||||||
|
* This method adds the "enqueue" (see {@link #addEnqueueEntriesIfNeeded()})
|
||||||
|
* and "start here" (see {@link #addStartHereEntries()} entries.
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addDefaultBeginningEntries() {
|
||||||
|
addEnqueueEntriesIfNeeded();
|
||||||
|
addStartHereEntries();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the entries which are usually at the bottom of the action list.
|
||||||
|
* @return the current {@link Builder} instance
|
||||||
|
*/
|
||||||
|
public Builder addDefaultEndEntries() {
|
||||||
|
addAllEntries(
|
||||||
|
StreamDialogDefaultEntry.APPEND_PLAYLIST,
|
||||||
|
StreamDialogDefaultEntry.SHARE,
|
||||||
|
StreamDialogDefaultEntry.OPEN_IN_BROWSER
|
||||||
|
);
|
||||||
|
addPlayWithKodiEntryIfNeeded();
|
||||||
|
addMarkAsWatchedEntryIfNeeded();
|
||||||
|
addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the {@link InfoItemDialog}.
|
||||||
|
* @return a new instance of {@link InfoItemDialog}
|
||||||
|
*/
|
||||||
|
public InfoItemDialog create() {
|
||||||
|
if (addDefaultEntriesAutomatically) {
|
||||||
|
addDefaultEndEntries();
|
||||||
|
}
|
||||||
|
return new InfoItemDialog(this.activity, this.fragment, this.infoItem, this.entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||||
|
final InfoItem item) {
|
||||||
|
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
||||||
|
throwable,
|
||||||
|
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||||
|
"none",
|
||||||
|
item.getServiceId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package org.schabi.newpipe.info_list.dialog;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
|
||||||
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
|
||||||
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* This enum provides entries that are accepted
|
||||||
|
* by the {@link InfoItemDialog.Builder}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* These entries contain a String {@link #resource} which is displayed in the dialog and
|
||||||
|
* a default {@link #action} that is executed
|
||||||
|
* when the entry is selected (via <code>onClick()</code>).
|
||||||
|
* <br/>
|
||||||
|
* They action can be overridden by using the Builder's
|
||||||
|
* {@link InfoItemDialog.Builder#setAction(
|
||||||
|
* StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}
|
||||||
|
* method.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public enum StreamDialogDefaultEntry {
|
||||||
|
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
|
||||||
|
fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
|
||||||
|
item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues the stream automatically to the current PlayerType.
|
||||||
|
*/
|
||||||
|
ENQUEUE(R.string.enqueue_stream, (fragment, item) ->
|
||||||
|
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
|
||||||
|
NavigationHelper.enqueueOnPlayer(fragment.getContext(), singlePlayQueue))
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues the stream automatically to the current PlayerType
|
||||||
|
* after the currently playing stream.
|
||||||
|
*/
|
||||||
|
ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragment, item) ->
|
||||||
|
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
|
||||||
|
NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), singlePlayQueue))
|
||||||
|
),
|
||||||
|
|
||||||
|
START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) ->
|
||||||
|
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(
|
||||||
|
fragment.getContext(), singlePlayQueue, true))),
|
||||||
|
|
||||||
|
START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragment, item) ->
|
||||||
|
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
|
||||||
|
NavigationHelper.playOnPopupPlayer(fragment.getContext(), singlePlayQueue, true))),
|
||||||
|
|
||||||
|
SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
|
||||||
|
throw new UnsupportedOperationException("This needs to be implemented manually "
|
||||||
|
+ "by using InfoItemDialog.Builder.setAction()");
|
||||||
|
}),
|
||||||
|
|
||||||
|
DELETE(R.string.delete, (fragment, item) -> {
|
||||||
|
throw new UnsupportedOperationException("This needs to be implemented manually "
|
||||||
|
+ "by using InfoItemDialog.Builder.setAction()");
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a {@link PlaylistDialog} to either append the stream to a playlist
|
||||||
|
* or create a new playlist if there are no local playlists.
|
||||||
|
*/
|
||||||
|
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
|
||||||
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
|
fragment.getContext(),
|
||||||
|
Collections.singletonList(new StreamEntity(item)),
|
||||||
|
dialog -> dialog.show(
|
||||||
|
fragment.getParentFragmentManager(),
|
||||||
|
"StreamDialogEntry@"
|
||||||
|
+ (dialog instanceof PlaylistAppendDialog ? "append" : "create")
|
||||||
|
+ "_playlist"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> {
|
||||||
|
final Uri videoUrl = Uri.parse(item.getUrl());
|
||||||
|
try {
|
||||||
|
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
SHARE(R.string.share, (fragment, item) ->
|
||||||
|
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||||
|
item.getThumbnailUrl())),
|
||||||
|
|
||||||
|
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
|
||||||
|
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
|
||||||
|
|
||||||
|
|
||||||
|
MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) ->
|
||||||
|
new HistoryRecordManager(fragment.getContext())
|
||||||
|
.markAsWatched(item)
|
||||||
|
.onErrorComplete()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
public final int resource;
|
||||||
|
@NonNull
|
||||||
|
public final StreamDialogEntry.StreamDialogEntryAction action;
|
||||||
|
|
||||||
|
StreamDialogDefaultEntry(@StringRes final int resource,
|
||||||
|
@NonNull final StreamDialogEntry.StreamDialogEntryAction action) {
|
||||||
|
this.resource = resource;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public StreamDialogEntry toStreamDialogEntry() {
|
||||||
|
return new StreamDialogEntry(resource, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package org.schabi.newpipe.info_list.dialog;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
|
public class StreamDialogEntry {
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
public final int resource;
|
||||||
|
@NonNull
|
||||||
|
public final StreamDialogEntryAction action;
|
||||||
|
|
||||||
|
public StreamDialogEntry(@StringRes final int resource,
|
||||||
|
@NonNull final StreamDialogEntryAction action) {
|
||||||
|
this.resource = resource;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(@NonNull final Context context) {
|
||||||
|
return context.getString(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface StreamDialogEntryAction {
|
||||||
|
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -11,10 +12,8 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import de.hdodenhof.circleimageview.CircleImageView;
|
|
||||||
|
|
||||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
public final CircleImageView itemThumbnailView;
|
public final ImageView itemThumbnailView;
|
||||||
public final TextView itemTitleView;
|
public final TextView itemTitleView;
|
||||||
private final TextView itemAdditionalDetailView;
|
private final TextView itemAdditionalDetailView;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.text.util.Linkify;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -28,8 +29,6 @@ import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import de.hdodenhof.circleimageview.CircleImageView;
|
|
||||||
|
|
||||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private static final String TAG = "CommentsMiniIIHolder";
|
private static final String TAG = "CommentsMiniIIHolder";
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private final int commentVerticalPadding;
|
private final int commentVerticalPadding;
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
private final RelativeLayout itemRoot;
|
||||||
public final CircleImageView itemThumbnailView;
|
public final ImageView itemThumbnailView;
|
||||||
private final TextView itemContentView;
|
private final TextView itemContentView;
|
||||||
private final TextView itemLikesCountView;
|
private final TextView itemLikesCountView;
|
||||||
private final TextView itemPublishedTime;
|
private final TextView itemPublishedTime;
|
||||||
|
|
|
@ -228,6 +228,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("FinalParameters")
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -300,6 +301,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("FinalParameters")
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|
|
@ -97,7 +97,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
if (getActivity() != null) {
|
if (getActivity() != null) {
|
||||||
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),
|
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),
|
||||||
|
|
|
@ -50,7 +50,6 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
import com.xwray.groupie.OnAsyncUpdateListener
|
|
||||||
import com.xwray.groupie.OnItemClickListener
|
import com.xwray.groupie.OnItemClickListener
|
||||||
import com.xwray.groupie.OnItemLongClickListener
|
import com.xwray.groupie.OnItemLongClickListener
|
||||||
import icepick.State
|
import icepick.State
|
||||||
|
@ -68,25 +67,21 @@ import org.schabi.newpipe.error.UserAction
|
||||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
|
||||||
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
|
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment
|
import org.schabi.newpipe.fragments.BaseStateFragment
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
||||||
import org.schabi.newpipe.ktx.slideUp
|
import org.schabi.newpipe.ktx.slideUp
|
||||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
class FeedFragment : BaseStateFragment<FeedState>() {
|
class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
|
@ -143,7 +138,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
val factory = FeedViewModel.Factory(requireContext(), groupId)
|
val factory = FeedViewModel.Factory(requireContext(), groupId)
|
||||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||||
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
||||||
|
|
||||||
groupAdapter = GroupieAdapter().apply {
|
groupAdapter = GroupieAdapter().apply {
|
||||||
setOnItemClickListener(listenerStreamItem)
|
setOnItemClickListener(listenerStreamItem)
|
||||||
|
@ -356,53 +351,12 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
feedBinding.loadingProgressBar.max = progressState.maxProgress
|
feedBinding.loadingProgressBar.max = progressState.maxProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showStreamDialog(item: StreamInfoItem) {
|
private fun showInfoItemDialog(item: StreamInfoItem) {
|
||||||
val context = context
|
val context = context
|
||||||
val activity: Activity? = getActivity()
|
val activity: Activity? = getActivity()
|
||||||
if (context == null || context.resources == null || activity == null) return
|
if (context == null || context.resources == null || activity == null) return
|
||||||
|
|
||||||
val entries = ArrayList<StreamDialogEntry>()
|
InfoItemDialog.Builder(activity, context, this, item).create().show()
|
||||||
if (PlayerHolder.getInstance().isPlayQueueReady) {
|
|
||||||
entries.add(StreamDialogEntry.enqueue)
|
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().queueSize > 1) {
|
|
||||||
entries.add(StreamDialogEntry.enqueue_next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.streamType == StreamType.AUDIO_STREAM) {
|
|
||||||
entries.addAll(
|
|
||||||
listOf(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share,
|
|
||||||
StreamDialogEntry.open_in_browser
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entries.addAll(
|
|
||||||
listOf(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.start_here_on_popup,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share,
|
|
||||||
StreamDialogEntry.open_in_browser
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// show "mark as watched" only when watch history is enabled
|
|
||||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
|
|
||||||
entries.add(
|
|
||||||
StreamDialogEntry.mark_as_watched
|
|
||||||
)
|
|
||||||
}
|
|
||||||
entries.add(StreamDialogEntry.show_channel_details)
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries)
|
|
||||||
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
|
|
||||||
StreamDialogEntry.clickOn(which, this, item)
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
|
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
|
||||||
|
@ -418,7 +372,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
|
|
||||||
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
|
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
|
||||||
if (item is StreamItem && !isRefreshing) {
|
if (item is StreamItem && !isRefreshing) {
|
||||||
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
|
showInfoItemDialog(item.streamWithState.stream.toStreamInfoItem())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -438,14 +392,11 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
// This need to be saved in a variable as the update occurs async
|
// This need to be saved in a variable as the update occurs async
|
||||||
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
|
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
|
||||||
|
|
||||||
groupAdapter.updateAsync(
|
groupAdapter.updateAsync(loadedState.items, false) {
|
||||||
loadedState.items, false,
|
|
||||||
OnAsyncUpdateListener {
|
|
||||||
oldOldestSubscriptionUpdate?.run {
|
oldOldestSubscriptionUpdate?.run {
|
||||||
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
|
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listState?.run {
|
listState?.run {
|
||||||
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
||||||
|
@ -497,8 +448,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{
|
{ subscriptionEntity ->
|
||||||
subscriptionEntity ->
|
|
||||||
handleFeedNotAvailable(
|
handleFeedNotAvailable(
|
||||||
subscriptionEntity,
|
subscriptionEntity,
|
||||||
t.cause,
|
t.cause,
|
||||||
|
|
|
@ -56,7 +56,7 @@ class FeedViewModel(
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
||||||
var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||||
feedDatabaseManager
|
feedDatabaseManager
|
||||||
.getStreams(groupId, showPlayedItems)
|
.getStreams(groupId, showPlayedItems)
|
||||||
.blockingGet(arrayListOf())
|
.blockingGet(arrayListOf())
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe.local.feed.notifications
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.work.BackoffPolicy
|
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
|
|
|
@ -84,7 +84,7 @@ public abstract class HistoryEntryAdapter<E, VH extends RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewRecycled(final VH holder) {
|
public void onViewRecycled(@NonNull final VH holder) {
|
||||||
super.onViewRecycled(holder);
|
super.onViewRecycled(holder);
|
||||||
holder.itemView.setOnClickListener(null);
|
holder.itemView.setOnClickListener(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.schabi.newpipe.local.history;
|
package org.schabi.newpipe.local.history;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
@ -29,20 +28,16 @@ import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -154,7 +149,7 @@ public class StatisticsPlaylistFragment
|
||||||
@Override
|
@Override
|
||||||
public void held(final LocalItem selectedItem) {
|
public void held(final LocalItem selectedItem) {
|
||||||
if (selectedItem instanceof StreamStatisticsEntry) {
|
if (selectedItem instanceof StreamStatisticsEntry) {
|
||||||
showStreamDialog((StreamStatisticsEntry) selectedItem);
|
showInfoItemDialog((StreamStatisticsEntry) selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -328,68 +323,32 @@ public class StatisticsPlaylistFragment
|
||||||
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
|
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showStreamDialog(final StreamStatisticsEntry item) {
|
private void showInfoItemDialog(final StreamStatisticsEntry item) {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (context == null || context.getResources() == null || activity == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||||
|
|
||||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
try {
|
||||||
|
final InfoItemDialog.Builder dialogBuilder =
|
||||||
|
new InfoItemDialog.Builder(getActivity(), context, this, infoItem);
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
// set entries in the middle; the others are added automatically
|
||||||
entries.add(StreamDialogEntry.enqueue);
|
dialogBuilder
|
||||||
|
.addEntry(StreamDialogDefaultEntry.DELETE)
|
||||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
.setAction(
|
||||||
entries.add(StreamDialogEntry.enqueue_next);
|
StreamDialogDefaultEntry.DELETE,
|
||||||
|
(f, i) -> deleteEntry(
|
||||||
|
Math.max(itemListAdapter.getItemsList().indexOf(item), 0)))
|
||||||
|
.setAction(
|
||||||
|
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
|
||||||
|
(f, i) -> NavigationHelper.playOnBackgroundPlayer(
|
||||||
|
context, getPlayQueueStartingAt(item), true))
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.delete,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.start_here_on_popup,
|
|
||||||
StreamDialogEntry.delete,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
}
|
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show "mark as watched" only when watch history is enabled
|
|
||||||
if (StreamDialogEntry.shouldAddMarkAsWatched(
|
|
||||||
item.getStreamEntity().getStreamType(),
|
|
||||||
context
|
|
||||||
)) {
|
|
||||||
entries.add(
|
|
||||||
StreamDialogEntry.mark_as_watched
|
|
||||||
);
|
|
||||||
}
|
|
||||||
entries.add(StreamDialogEntry.show_channel_details);
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
|
||||||
|
|
||||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
|
|
||||||
NavigationHelper
|
|
||||||
.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
|
|
||||||
StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) ->
|
|
||||||
deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0)));
|
|
||||||
|
|
||||||
new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context),
|
|
||||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteEntry(final int index) {
|
private void deleteEntry(final int index) {
|
||||||
final LocalItem infoItem = itemListAdapter.getItemsList().get(index);
|
final LocalItem infoItem = itemListAdapter.getItemsList().get(index);
|
||||||
if (infoItem instanceof StreamStatisticsEntry) {
|
if (infoItem instanceof StreamStatisticsEntry) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.schabi.newpipe.local.playlist;
|
package org.schabi.newpipe.local.playlist;
|
||||||
|
|
||||||
import android.app.Activity;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -38,22 +40,18 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -68,9 +66,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
|
||||||
|
|
||||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
||||||
// Save the list 10 seconds after the last change occurred
|
// Save the list 10 seconds after the last change occurred
|
||||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||||
|
@ -182,7 +177,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
@Override
|
@Override
|
||||||
public void held(final LocalItem selectedItem) {
|
public void held(final LocalItem selectedItem) {
|
||||||
if (selectedItem instanceof PlaylistStreamEntry) {
|
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||||
showStreamItemDialog((PlaylistStreamEntry) selectedItem);
|
showInfoItemDialog((PlaylistStreamEntry) selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,7 +350,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
new AlertDialog.Builder(requireContext())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.remove_watched_popup_warning)
|
.setMessage(R.string.remove_watched_popup_warning)
|
||||||
.setTitle(R.string.remove_watched_popup_title)
|
.setTitle(R.string.remove_watched_popup_title)
|
||||||
.setPositiveButton(R.string.yes,
|
.setPositiveButton(R.string.ok,
|
||||||
(DialogInterface d, int id) -> removeWatchedStreams(false))
|
(DialogInterface d, int id) -> removeWatchedStreams(false))
|
||||||
.setNeutralButton(
|
.setNeutralButton(
|
||||||
R.string.remove_watched_popup_yes_and_partially_watched_videos,
|
R.string.remove_watched_popup_yes_and_partially_watched_videos,
|
||||||
|
@ -743,70 +738,39 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
|
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void showStreamItemDialog(final PlaylistStreamEntry item) {
|
protected void showInfoItemDialog(final PlaylistStreamEntry item) {
|
||||||
final Context context = getContext();
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (context == null || context.getResources() == null || activity == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||||
|
|
||||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
try {
|
||||||
|
final Context context = getContext();
|
||||||
|
final InfoItemDialog.Builder dialogBuilder =
|
||||||
|
new InfoItemDialog.Builder(getActivity(), context, this, infoItem);
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
// add entries in the middle
|
||||||
entries.add(StreamDialogEntry.enqueue);
|
dialogBuilder.addAllEntries(
|
||||||
|
StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL,
|
||||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
StreamDialogDefaultEntry.DELETE
|
||||||
entries.add(StreamDialogEntry.enqueue_next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.set_as_playlist_thumbnail,
|
|
||||||
StreamDialogEntry.delete,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entries.addAll(Arrays.asList(
|
|
||||||
StreamDialogEntry.start_here_on_background,
|
|
||||||
StreamDialogEntry.start_here_on_popup,
|
|
||||||
StreamDialogEntry.set_as_playlist_thumbnail,
|
|
||||||
StreamDialogEntry.delete,
|
|
||||||
StreamDialogEntry.append_playlist,
|
|
||||||
StreamDialogEntry.share
|
|
||||||
));
|
|
||||||
}
|
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show "mark as watched" only when watch history is enabled
|
|
||||||
if (StreamDialogEntry.shouldAddMarkAsWatched(
|
|
||||||
item.getStreamEntity().getStreamType(),
|
|
||||||
context
|
|
||||||
)) {
|
|
||||||
entries.add(
|
|
||||||
StreamDialogEntry.mark_as_watched
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// set custom actions
|
||||||
|
// all entries modified below have already been added within the builder
|
||||||
|
dialogBuilder
|
||||||
|
.setAction(
|
||||||
|
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
|
||||||
|
(f, i) -> NavigationHelper.playOnBackgroundPlayer(
|
||||||
|
context, getPlayQueueStartingAt(item), true))
|
||||||
|
.setAction(
|
||||||
|
StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL,
|
||||||
|
(f, i) ->
|
||||||
|
changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl()))
|
||||||
|
.setAction(
|
||||||
|
StreamDialogDefaultEntry.DELETE,
|
||||||
|
(f, i) -> deleteItem(item))
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem);
|
||||||
}
|
}
|
||||||
entries.add(StreamDialogEntry.show_channel_details);
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
|
||||||
|
|
||||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(context,
|
|
||||||
getPlayQueueStartingAt(item), true));
|
|
||||||
StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction(
|
|
||||||
(fragment, infoItemDuplicate) ->
|
|
||||||
changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl()));
|
|
||||||
StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) ->
|
|
||||||
deleteItem(item));
|
|
||||||
|
|
||||||
new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context),
|
|
||||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInitialData(final long pid, final String title) {
|
private void setInitialData(final long pid, final String title) {
|
||||||
|
|
|
@ -61,7 +61,7 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Icepick.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.functions.BiFunction
|
|
||||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
@ -33,9 +32,8 @@ class FeedGroupDialogViewModel(
|
||||||
private var subscriptionsFlowable = Flowable
|
private var subscriptionsFlowable = Flowable
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
filterSubscriptions.startWithItem(initialQuery),
|
filterSubscriptions.startWithItem(initialQuery),
|
||||||
toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped),
|
toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped)
|
||||||
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
|
) { t1: String, t2: Boolean -> Filter(t1, t2) }
|
||||||
)
|
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.switchMap { (query, showOnlyUngrouped) ->
|
.switchMap { (query, showOnlyUngrouped) ->
|
||||||
subscriptionManager.getSubscriptions(groupId, query, showOnlyUngrouped)
|
subscriptionManager.getSubscriptions(groupId, query, showOnlyUngrouped)
|
||||||
|
@ -56,9 +54,8 @@ class FeedGroupDialogViewModel(
|
||||||
|
|
||||||
private var subscriptionsDisposable = Flowable
|
private var subscriptionsDisposable = Flowable
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
|
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId)
|
||||||
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
|
) { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
|
||||||
)
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe(mutableSubscriptionsLiveData::postValue)
|
.subscribe(mutableSubscriptionsLiveData::postValue)
|
||||||
|
|
||||||
|
|
|
@ -57,15 +57,12 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
|
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
|
||||||
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
|
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
|
||||||
viewModel.dialogEventLiveData.observe(
|
viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
|
||||||
viewLifecycleOwner,
|
|
||||||
Observer {
|
|
||||||
when (it) {
|
when (it) {
|
||||||
ProcessingEvent -> disableInput()
|
ProcessingEvent -> disableInput()
|
||||||
SuccessEvent -> dismiss()
|
SuccessEvent -> dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext())
|
binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.feedGroupsList.adapter = groupAdapter
|
binding.feedGroupsList.adapter = groupAdapter
|
||||||
|
|
|
@ -25,7 +25,6 @@ import com.grack.nanojson.JsonAppendableWriter;
|
||||||
import com.grack.nanojson.JsonArray;
|
import com.grack.nanojson.JsonArray;
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
import com.grack.nanojson.JsonParser;
|
import com.grack.nanojson.JsonParser;
|
||||||
import com.grack.nanojson.JsonSink;
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
|
@ -125,10 +124,11 @@ public final class ImportExportJsonHelper {
|
||||||
/**
|
/**
|
||||||
* @see #writeTo(List, OutputStream, ImportExportEventListener)
|
* @see #writeTo(List, OutputStream, ImportExportEventListener)
|
||||||
* @param items the list of subscriptions items
|
* @param items the list of subscriptions items
|
||||||
* @param writer the output {@link JsonSink}
|
* @param writer the output {@link JsonAppendableWriter}
|
||||||
* @param eventListener listener for the events generated
|
* @param eventListener listener for the events generated
|
||||||
*/
|
*/
|
||||||
public static void writeTo(final List<SubscriptionItem> items, final JsonSink writer,
|
public static void writeTo(final List<SubscriptionItem> items,
|
||||||
|
final JsonAppendableWriter writer,
|
||||||
@Nullable final ImportExportEventListener eventListener) {
|
@Nullable final ImportExportEventListener eventListener) {
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
eventListener.onSizeReceived(items.size());
|
eventListener.onSizeReceived(items.size());
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -23,11 +26,9 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
@ -42,13 +43,6 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
public final class PlayQueueActivity extends AppCompatActivity
|
public final class PlayQueueActivity extends AppCompatActivity
|
||||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||||
View.OnClickListener, PlaybackParameterDialog.Callback {
|
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||||
|
@ -129,7 +123,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_append_playlist:
|
case R.id.action_append_playlist:
|
||||||
appendAllToPlaylist();
|
player.onAddToPlaylistClicked(getSupportFragmentManager());
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_playback_speed:
|
case R.id.action_playback_speed:
|
||||||
openPlaybackParameterDialog();
|
openPlaybackParameterDialog();
|
||||||
|
@ -443,24 +437,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
seeking = false;
|
seeking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Playlist append
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private void appendAllToPlaylist() {
|
|
||||||
if (player != null && player.getPlayQueue() != null) {
|
|
||||||
openPlaylistAppendDialog(player.getPlayQueue().getStreams());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openPlaylistAppendDialog(final List<PlayQueueItem> playQueueItems) {
|
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
|
||||||
getApplicationContext(),
|
|
||||||
playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()),
|
|
||||||
dialog -> dialog.show(getSupportFragmentManager(), TAG)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Binding Service Listener
|
// Binding Service Listener
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -624,7 +600,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
//2) Icon change accordingly to current App Theme
|
//2) Icon change accordingly to current App Theme
|
||||||
// using rootView.getContext() because getApplicationContext() didn't work
|
// using rootView.getContext() because getApplicationContext() didn't work
|
||||||
final Context context = queueControlBinding.getRoot().getContext();
|
|
||||||
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
|
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,6 @@ import android.provider.Settings;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.ContextThemeWrapper;
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
@ -89,7 +88,6 @@ import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.PopupMenu;
|
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
|
@ -99,12 +97,15 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper;
|
||||||
import androidx.appcompat.widget.AppCompatImageButton;
|
import androidx.appcompat.widget.AppCompatImageButton;
|
||||||
|
import androidx.appcompat.widget.PopupMenu;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.GestureDetectorCompat;
|
import androidx.core.view.GestureDetectorCompat;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
@ -112,6 +113,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player.PositionInfo;
|
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
|
@ -122,6 +124,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||||
|
@ -136,6 +139,7 @@ import com.squareup.picasso.Target;
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||||
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
|
@ -144,11 +148,13 @@ import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.DisplayPortion;
|
import org.schabi.newpipe.player.event.DisplayPortion;
|
||||||
|
@ -158,9 +164,10 @@ import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||||
import org.schabi.newpipe.player.helper.LoadController;
|
import org.schabi.newpipe.player.helper.LoadController;
|
||||||
import org.schabi.newpipe.player.helper.MediaSessionManager;
|
import org.schabi.newpipe.player.helper.MediaSessionManager;
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener;
|
||||||
|
import org.schabi.newpipe.player.listeners.view.QualityClickListener;
|
||||||
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
||||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||||
|
@ -175,6 +182,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||||
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
@ -193,6 +201,8 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
@ -521,9 +531,12 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initListeners() {
|
private void initListeners() {
|
||||||
|
binding.qualityTextView.setOnClickListener(
|
||||||
|
new QualityClickListener(this, qualityPopupMenu));
|
||||||
|
binding.playbackSpeed.setOnClickListener(
|
||||||
|
new PlaybackSpeedClickListener(this, playbackSpeedPopupMenu));
|
||||||
|
|
||||||
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
||||||
binding.playbackSpeed.setOnClickListener(this);
|
|
||||||
binding.qualityTextView.setOnClickListener(this);
|
|
||||||
binding.captionTextView.setOnClickListener(this);
|
binding.captionTextView.setOnClickListener(this);
|
||||||
binding.resizeTextView.setOnClickListener(this);
|
binding.resizeTextView.setOnClickListener(this);
|
||||||
binding.playbackLiveSync.setOnClickListener(this);
|
binding.playbackLiveSync.setOnClickListener(this);
|
||||||
|
@ -532,10 +545,15 @@ public final class Player implements
|
||||||
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
||||||
binding.getRoot().setOnTouchListener(playerGestureListener);
|
binding.getRoot().setOnTouchListener(playerGestureListener);
|
||||||
|
|
||||||
binding.queueButton.setOnClickListener(this);
|
binding.queueButton.setOnClickListener(v -> onQueueClicked());
|
||||||
binding.segmentsButton.setOnClickListener(this);
|
binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
|
||||||
binding.repeatButton.setOnClickListener(this);
|
binding.repeatButton.setOnClickListener(v -> onRepeatClicked());
|
||||||
binding.shuffleButton.setOnClickListener(this);
|
binding.shuffleButton.setOnClickListener(v -> onShuffleClicked());
|
||||||
|
binding.addToPlaylistButton.setOnClickListener(v -> {
|
||||||
|
if (getParentActivity() != null) {
|
||||||
|
onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
binding.playPauseButton.setOnClickListener(this);
|
binding.playPauseButton.setOnClickListener(this);
|
||||||
binding.playPreviousButton.setOnClickListener(this);
|
binding.playPreviousButton.setOnClickListener(this);
|
||||||
|
@ -580,11 +598,17 @@ public final class Player implements
|
||||||
v.getPaddingTop(),
|
v.getPaddingTop(),
|
||||||
v.getPaddingRight(),
|
v.getPaddingRight(),
|
||||||
v.getPaddingBottom());
|
v.getPaddingBottom());
|
||||||
binding.fastSeekOverlay.setPadding(
|
|
||||||
v.getPaddingLeft(),
|
// If we added padding to the fast seek overlay, too, it would not go under the
|
||||||
v.getPaddingTop(),
|
// system ui. Instead we apply negative margins equal to the window insets of
|
||||||
v.getPaddingRight(),
|
// the opposite side, so that the view covers all of the player (overflowing on
|
||||||
v.getPaddingBottom());
|
// some sides) and its center coincides with the center of other controls.
|
||||||
|
final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams)
|
||||||
|
binding.fastSeekOverlay.getLayoutParams();
|
||||||
|
fastSeekParams.leftMargin = -v.getPaddingRight();
|
||||||
|
fastSeekParams.topMargin = -v.getPaddingBottom();
|
||||||
|
fastSeekParams.rightMargin = -v.getPaddingLeft();
|
||||||
|
fastSeekParams.bottomMargin = -v.getPaddingTop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -593,8 +617,7 @@ public final class Player implements
|
||||||
*/
|
*/
|
||||||
private void setupPlayerSeekOverlay() {
|
private void setupPlayerSeekOverlay() {
|
||||||
binding.fastSeekOverlay
|
binding.fastSeekOverlay
|
||||||
.seekSecondsSupplier(
|
.seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(this) / 1000)
|
||||||
() -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f))
|
|
||||||
.performListener(new PlayerFastSeekOverlay.PerformListener() {
|
.performListener(new PlayerFastSeekOverlay.PerformListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -607,6 +630,7 @@ public final class Player implements
|
||||||
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
|
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public FastSeekDirection getFastSeekDirection(
|
public FastSeekDirection getFastSeekDirection(
|
||||||
@NonNull final DisplayPortion portion
|
@NonNull final DisplayPortion portion
|
||||||
|
@ -658,6 +682,7 @@ public final class Player implements
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Playback initialization via intent
|
//region Playback initialization via intent
|
||||||
|
|
||||||
|
@SuppressWarnings("MethodLength")
|
||||||
public void handleIntent(@NonNull final Intent intent) {
|
public void handleIntent(@NonNull final Intent intent) {
|
||||||
// fail fast if no play queue was provided
|
// fail fast if no play queue was provided
|
||||||
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
|
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
|
||||||
|
@ -1910,7 +1935,7 @@ public final class Player implements
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showHideShadow(final boolean show, final long duration) {
|
public void showHideShadow(final boolean show, final long duration) {
|
||||||
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
|
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||||
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
|
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||||
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||||
|
@ -2378,6 +2403,32 @@ public final class Player implements
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist append
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
//region Playlist append
|
||||||
|
|
||||||
|
public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onAddToPlaylistClicked() called");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getPlayQueue() != null) {
|
||||||
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
|
getContext(),
|
||||||
|
getPlayQueue()
|
||||||
|
.getStreams()
|
||||||
|
.stream()
|
||||||
|
.map(StreamEntity::new)
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
dialog -> dialog.show(fragmentManager, TAG)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Mute / Unmute
|
// Mute / Unmute
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -2443,8 +2494,8 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(
|
public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
|
||||||
final PositionInfo oldPosition, final PositionInfo newPosition,
|
@NonNull final PositionInfo newPosition,
|
||||||
@DiscontinuityReason final int discontinuityReason) {
|
@DiscontinuityReason final int discontinuityReason) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
||||||
|
@ -2493,7 +2544,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCues(final List<Cue> cues) {
|
public void onCues(@NonNull final List<Cue> cues) {
|
||||||
binding.subtitleView.onCues(cues);
|
binding.subtitleView.onCues(cues);
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
@ -2999,18 +3050,19 @@ public final class Player implements
|
||||||
|
|
||||||
final MediaSourceTag metadata;
|
final MediaSourceTag metadata;
|
||||||
try {
|
try {
|
||||||
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
|
final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem();
|
||||||
} catch (IndexOutOfBoundsException | ClassCastException error) {
|
if (currentMediaItem == null || currentMediaItem.playbackProperties == null
|
||||||
|
|| currentMediaItem.playbackProperties.tag == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag;
|
||||||
|
} catch (final IndexOutOfBoundsException | ClassCastException ex) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Could not update metadata: " + error.getMessage());
|
Log.d(TAG, "Could not update metadata", ex);
|
||||||
error.printStackTrace();
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
maybeAutoQueueNextStream(metadata);
|
maybeAutoQueueNextStream(metadata);
|
||||||
|
|
||||||
if (currentMetadata == metadata) {
|
if (currentMetadata == metadata) {
|
||||||
|
@ -3119,6 +3171,7 @@ public final class Player implements
|
||||||
binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
|
binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
|
||||||
binding.shuffleButton.setVisibility(View.VISIBLE);
|
binding.shuffleButton.setVisibility(View.VISIBLE);
|
||||||
binding.repeatButton.setVisibility(View.VISIBLE);
|
binding.repeatButton.setVisibility(View.VISIBLE);
|
||||||
|
binding.addToPlaylistButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
hideControls(0, 0);
|
hideControls(0, 0);
|
||||||
binding.itemsListPanel.requestFocus();
|
binding.itemsListPanel.requestFocus();
|
||||||
|
@ -3156,6 +3209,7 @@ public final class Player implements
|
||||||
binding.itemsListHeaderDuration.setVisibility(View.GONE);
|
binding.itemsListHeaderDuration.setVisibility(View.GONE);
|
||||||
binding.shuffleButton.setVisibility(View.GONE);
|
binding.shuffleButton.setVisibility(View.GONE);
|
||||||
binding.repeatButton.setVisibility(View.GONE);
|
binding.repeatButton.setVisibility(View.GONE);
|
||||||
|
binding.addToPlaylistButton.setVisibility(View.GONE);
|
||||||
|
|
||||||
hideControls(0, 0);
|
hideControls(0, 0);
|
||||||
binding.itemsListPanel.requestFocus();
|
binding.itemsListPanel.requestFocus();
|
||||||
|
@ -3184,6 +3238,7 @@ public final class Player implements
|
||||||
|
|
||||||
binding.shuffleButton.setVisibility(View.GONE);
|
binding.shuffleButton.setVisibility(View.GONE);
|
||||||
binding.repeatButton.setVisibility(View.GONE);
|
binding.repeatButton.setVisibility(View.GONE);
|
||||||
|
binding.addToPlaylistButton.setVisibility(View.GONE);
|
||||||
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
|
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3203,6 +3258,9 @@ public final class Player implements
|
||||||
binding.itemsListPanel.setTranslationY(
|
binding.itemsListPanel.setTranslationY(
|
||||||
-binding.itemsListPanel.getHeight() * 5);
|
-binding.itemsListPanel.getHeight() * 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// clear focus, otherwise a white rectangle remains on top of the player
|
||||||
|
binding.itemsListClose.clearFocus();
|
||||||
binding.playPauseButton.requestFocus();
|
binding.playPauseButton.requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3286,7 +3344,27 @@ public final class Player implements
|
||||||
@Override // own playback listener
|
@Override // own playback listener
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
||||||
return (isAudioOnly ? audioResolver : videoResolver).resolve(info);
|
if (audioPlayerSelected()) {
|
||||||
|
return audioResolver.resolve(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAudioOnly && videoResolver.getStreamSourceType().orElse(
|
||||||
|
SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY)
|
||||||
|
== SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) {
|
||||||
|
// If the current info has only video streams with audio and if the stream is played as
|
||||||
|
// audio, we need to use the audio resolver, otherwise the video stream will be played
|
||||||
|
// in background.
|
||||||
|
return audioResolver.resolve(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if the stream is played in background, we need to use the video resolver if the
|
||||||
|
// info played is separated video-only and audio-only streams; otherwise, if the audio
|
||||||
|
// resolver was called when the app was in background, the app will only stream audio when
|
||||||
|
// the user come back to the app and will never fetch the video stream.
|
||||||
|
// Note that the video is not fetched when the app is in background because the video
|
||||||
|
// renderer is fully disabled (see useVideoSource method), except for HLS streams
|
||||||
|
// (see https://github.com/google/ExoPlayer/issues/9282).
|
||||||
|
return videoResolver.resolve(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disablePreloadingOfCurrentTrack() {
|
public void disablePreloadingOfCurrentTrack() {
|
||||||
|
@ -3538,37 +3616,6 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onQualitySelectorClicked() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onQualitySelectorClicked() called");
|
|
||||||
}
|
|
||||||
qualityPopupMenu.show();
|
|
||||||
isSomePopupMenuVisible = true;
|
|
||||||
|
|
||||||
final VideoStream videoStream = getSelectedVideoStream();
|
|
||||||
if (videoStream != null) {
|
|
||||||
final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " "
|
|
||||||
+ videoStream.resolution;
|
|
||||||
binding.qualityTextView.setText(qualityText);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveWasPlaying();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onPlaybackSpeedClicked() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onPlaybackSpeedClicked() called");
|
|
||||||
}
|
|
||||||
if (videoPlayerSelected()) {
|
|
||||||
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch(),
|
|
||||||
getPlaybackSkipSilence(), this::setPlaybackParameters)
|
|
||||||
.show(getParentActivity().getSupportFragmentManager(), null);
|
|
||||||
} else {
|
|
||||||
playbackSpeedPopupMenu.show();
|
|
||||||
isSomePopupMenuVisible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCaptionClicked() {
|
private void onCaptionClicked() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onCaptionClicked() called");
|
Log.d(TAG, "onCaptionClicked() called");
|
||||||
|
@ -3673,11 +3720,7 @@ public final class Player implements
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
}
|
}
|
||||||
if (v.getId() == binding.qualityTextView.getId()) {
|
if (v.getId() == binding.resizeTextView.getId()) {
|
||||||
onQualitySelectorClicked();
|
|
||||||
} else if (v.getId() == binding.playbackSpeed.getId()) {
|
|
||||||
onPlaybackSpeedClicked();
|
|
||||||
} else if (v.getId() == binding.resizeTextView.getId()) {
|
|
||||||
onResizeClicked();
|
onResizeClicked();
|
||||||
} else if (v.getId() == binding.captionTextView.getId()) {
|
} else if (v.getId() == binding.captionTextView.getId()) {
|
||||||
onCaptionClicked();
|
onCaptionClicked();
|
||||||
|
@ -3689,18 +3732,6 @@ public final class Player implements
|
||||||
playPrevious();
|
playPrevious();
|
||||||
} else if (v.getId() == binding.playNextButton.getId()) {
|
} else if (v.getId() == binding.playNextButton.getId()) {
|
||||||
playNext();
|
playNext();
|
||||||
} else if (v.getId() == binding.queueButton.getId()) {
|
|
||||||
onQueueClicked();
|
|
||||||
return;
|
|
||||||
} else if (v.getId() == binding.segmentsButton.getId()) {
|
|
||||||
onSegmentsClicked();
|
|
||||||
return;
|
|
||||||
} else if (v.getId() == binding.repeatButton.getId()) {
|
|
||||||
onRepeatClicked();
|
|
||||||
return;
|
|
||||||
} else if (v.getId() == binding.shuffleButton.getId()) {
|
|
||||||
onShuffleClicked();
|
|
||||||
return;
|
|
||||||
} else if (v.getId() == binding.moreOptionsButton.getId()) {
|
} else if (v.getId() == binding.moreOptionsButton.getId()) {
|
||||||
onMoreOptionsClicked();
|
onMoreOptionsClicked();
|
||||||
} else if (v.getId() == binding.share.getId()) {
|
} else if (v.getId() == binding.share.getId()) {
|
||||||
|
@ -3729,7 +3760,18 @@ public final class Player implements
|
||||||
context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
|
context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentState != STATE_COMPLETED) {
|
manageControlsAfterOnClick(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the controls after a click occurred on the player UI.
|
||||||
|
* @param v – The view that was clicked
|
||||||
|
*/
|
||||||
|
public void manageControlsAfterOnClick(@NonNull final View v) {
|
||||||
|
if (currentState == STATE_COMPLETED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||||
showHideShadow(true, DEFAULT_CONTROLS_DURATION);
|
showHideShadow(true, DEFAULT_CONTROLS_DURATION);
|
||||||
animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
|
animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
|
||||||
|
@ -3746,7 +3788,6 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(final View v) {
|
public boolean onLongClick(final View v) {
|
||||||
|
@ -3767,6 +3808,10 @@ public final class Player implements
|
||||||
case KeyEvent.KEYCODE_SPACE:
|
case KeyEvent.KEYCODE_SPACE:
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
playPause();
|
playPause();
|
||||||
|
if (isPlaying()) {
|
||||||
|
hideControls(0, 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BACK:
|
case KeyEvent.KEYCODE_BACK:
|
||||||
|
@ -3780,8 +3825,9 @@ public final class Player implements
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||||
if (binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) {
|
if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus())
|
||||||
// do not interfere with focus in playlist etc.
|
|| isQueueVisible) {
|
||||||
|
// do not interfere with focus in playlist and play queue etc.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3789,15 +3835,13 @@ public final class Player implements
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isControlsVisible()) {
|
if (isControlsVisible()) {
|
||||||
if (!isQueueVisible) {
|
hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
|
||||||
|
} else {
|
||||||
binding.playPauseButton.requestFocus();
|
binding.playPauseButton.requestFocus();
|
||||||
}
|
|
||||||
showControlsThenHide();
|
showControlsThenHide();
|
||||||
showSystemUIPartially();
|
showSystemUIPartially();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -4141,19 +4185,125 @@ public final class Player implements
|
||||||
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
|
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void useVideoSource(final boolean video) {
|
private void useVideoSource(final boolean videoEnabled) {
|
||||||
if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) {
|
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isAudioOnly = !video;
|
isAudioOnly = !videoEnabled;
|
||||||
// When a user returns from background controls could be hidden
|
// When a user returns from background, controls could be hidden but SystemUI will be shown
|
||||||
// but systemUI will be shown 100%. Hide it
|
// 100%. Hide it.
|
||||||
if (!isAudioOnly && !isControlsVisible()) {
|
if (!isAudioOnly && !isControlsVisible()) {
|
||||||
hideSystemUIIfNeeded();
|
hideSystemUIIfNeeded();
|
||||||
}
|
}
|
||||||
setRecovery();
|
|
||||||
|
// The current metadata may be null sometimes (for e.g. when using an unstable connection
|
||||||
|
// in livestreams) so we will be not able to execute the block below.
|
||||||
|
// Reload the play queue manager in this case, which is the behavior when we don't know the
|
||||||
|
// index of the video renderer or playQueueManagerReloadingNeeded returns true.
|
||||||
|
if (currentMetadata == null) {
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
|
setRecovery();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int videoRenderIndex = getVideoRendererIndex();
|
||||||
|
final StreamInfo info = currentMetadata.getMetadata();
|
||||||
|
|
||||||
|
// In the case we don't know the source type, fallback to the one with video with audio or
|
||||||
|
// audio-only source.
|
||||||
|
final SourceType sourceType = videoResolver.getStreamSourceType().orElse(
|
||||||
|
SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY);
|
||||||
|
|
||||||
|
if (playQueueManagerReloadingNeeded(sourceType, info, videoRenderIndex)) {
|
||||||
|
reloadPlayQueueManager();
|
||||||
|
} else {
|
||||||
|
final StreamType streamType = info.getStreamType();
|
||||||
|
if (streamType == StreamType.AUDIO_STREAM
|
||||||
|
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
|
||||||
|
// Nothing to do more than setting the recovery position
|
||||||
|
setRecovery();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
|
||||||
|
trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex);
|
||||||
|
if (videoEnabled) {
|
||||||
|
// Clearing the null selection override enable again the video stream (and its
|
||||||
|
// fetching).
|
||||||
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||||
|
.clearSelectionOverride(videoRenderIndex, videoTrackGroupArray));
|
||||||
|
} else {
|
||||||
|
// Using setRendererDisabled still fetch the video stream in background, contrary
|
||||||
|
// to setSelectionOverride with a null override.
|
||||||
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||||
|
.setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRecovery();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the play queue manager needs to be reloaded when switching player type.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The play queue manager needs to be reloaded if the video renderer index is not known and if
|
||||||
|
* the content is not an audio content, but also if none of the following cases is met:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
|
||||||
|
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
|
||||||
|
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
||||||
|
* {@link SourceType#LIVE_STREAM live source};</li>
|
||||||
|
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
||||||
|
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
||||||
|
* {@link StreamType#LIVE_STREAM live stream} or a
|
||||||
|
* {@link StreamType#LIVE_STREAM live stream}.
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param sourceType the {@link SourceType} of the stream
|
||||||
|
* @param streamInfo the {@link StreamInfo} of the stream
|
||||||
|
* @param videoRendererIndex the video renderer index of the video source, if that's a video
|
||||||
|
* source (or {@link #RENDERER_UNAVAILABLE})
|
||||||
|
* @return whether the play queue manager needs to be reloaded
|
||||||
|
*/
|
||||||
|
private boolean playQueueManagerReloadingNeeded(final SourceType sourceType,
|
||||||
|
@NonNull final StreamInfo streamInfo,
|
||||||
|
final int videoRendererIndex) {
|
||||||
|
final StreamType streamType = streamInfo.getStreamType();
|
||||||
|
|
||||||
|
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
|
||||||
|
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The content is an audio stream, an audio live stream, or a live stream with a live
|
||||||
|
// source: it's not needed to reload the play queue manager because the stream source will
|
||||||
|
// be the same
|
||||||
|
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|
||||||
|
|| (streamType == StreamType.LIVE_STREAM
|
||||||
|
&& sourceType == SourceType.LIVE_STREAM)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The content's source is a video with separated audio or a video with audio -> the video
|
||||||
|
// and its fetch may be disabled
|
||||||
|
// The content's source is a video with embedded audio and the content has no separated
|
||||||
|
// audio stream available: it's probably not needed to reload the play queue manager
|
||||||
|
// because the stream source will be probably the same as the current played
|
||||||
|
if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO
|
||||||
|
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||||
|
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||||
|
// It's not needed to reload the play queue manager only if the content's stream type
|
||||||
|
// is a video stream or a live stream
|
||||||
|
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other cases: the play queue manager reload is needed
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -4191,7 +4341,7 @@ public final class Player implements
|
||||||
private boolean isLive() {
|
private boolean isLive() {
|
||||||
try {
|
try {
|
||||||
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic();
|
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic();
|
||||||
} catch (@NonNull final IndexOutOfBoundsException e) {
|
} catch (final IndexOutOfBoundsException e) {
|
||||||
// Why would this even happen =(... but lets log it anyway, better safe than sorry
|
// Why would this even happen =(... but lets log it anyway, better safe than sorry
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e);
|
Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e);
|
||||||
|
@ -4263,6 +4413,10 @@ public final class Player implements
|
||||||
return isSomePopupMenuVisible;
|
return isSomePopupMenuVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSomePopupMenuVisible(final boolean somePopupMenuVisible) {
|
||||||
|
isSomePopupMenuVisible = somePopupMenuVisible;
|
||||||
|
}
|
||||||
|
|
||||||
public ImageButton getPlayPauseButton() {
|
public ImageButton getPlayPauseButton() {
|
||||||
return binding.playPauseButton;
|
return binding.playPauseButton;
|
||||||
}
|
}
|
||||||
|
@ -4344,6 +4498,11 @@ public final class Player implements
|
||||||
public PlayQueueAdapter getPlayQueueAdapter() {
|
public PlayQueueAdapter getPlayQueueAdapter() {
|
||||||
return playQueueAdapter;
|
return playQueueAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PlayerBinding getBinding() {
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -4369,8 +4528,8 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanupVideoSurface() {
|
private void cleanupVideoSurface() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
|
// Only for API >= 23
|
||||||
if (surfaceHolderCallback != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
|
binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
|
||||||
}
|
}
|
||||||
|
@ -4378,6 +4537,33 @@ public final class Player implements
|
||||||
surfaceHolderCallback = null;
|
surfaceHolderCallback = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the video renderer index of the current playing stream.
|
||||||
|
*
|
||||||
|
* This method returns the video renderer index of the current
|
||||||
|
* {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current
|
||||||
|
* {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index.
|
||||||
|
*
|
||||||
|
* @return the video renderer index or {@link #RENDERER_UNAVAILABLE} if it cannot be get
|
||||||
|
*/
|
||||||
|
private int getVideoRendererIndex() {
|
||||||
|
final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector
|
||||||
|
.getCurrentMappedTrackInfo();
|
||||||
|
|
||||||
|
if (mappedTrackInfo == null) {
|
||||||
|
return RENDERER_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check every renderer
|
||||||
|
return IntStream.range(0, mappedTrackInfo.getRendererCount())
|
||||||
|
// Check the renderer is a video renderer and has at least one track
|
||||||
|
.filter(i -> !mappedTrackInfo.getTrackGroups(i).isEmpty()
|
||||||
|
&& simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO)
|
||||||
|
// Return the first index found (there is at most one renderer per renderer type)
|
||||||
|
.findFirst()
|
||||||
|
// No video renderer index with at least one track found: return unavailable index
|
||||||
|
.orElse(RENDERER_UNAVAILABLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,7 +149,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) {
|
public void onAudioSessionIdChanged(@NonNull final EventTime eventTime,
|
||||||
|
final int audioSessionId) {
|
||||||
notifyAudioSessionUpdate(true, audioSessionId);
|
notifyAudioSessionUpdate(true, audioSessionId);
|
||||||
}
|
}
|
||||||
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||||
|
|
|
@ -63,6 +63,7 @@ import java.io.File;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public DataSource createDataSource() {
|
public DataSource createDataSource() {
|
||||||
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
||||||
|
@ -86,8 +87,8 @@ import java.io.File;
|
||||||
|
|
||||||
Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
|
Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
|
||||||
}
|
}
|
||||||
} catch (final Exception ignored) {
|
} catch (final Exception e) {
|
||||||
Log.e(TAG, "Failed to delete file.", ignored);
|
Log.e(TAG, "Failed to delete file.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,9 +135,7 @@ public class MediaSessionManager {
|
||||||
lastTitleHashCode = title.hashCode();
|
lastTitleHashCode = title.hashCode();
|
||||||
lastArtistHashCode = artist.hashCode();
|
lastArtistHashCode = artist.hashCode();
|
||||||
lastDuration = duration;
|
lastDuration = duration;
|
||||||
if (optAlbumArt.isPresent()) {
|
optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode());
|
||||||
lastAlbumArtHashCode = optAlbumArt.get().hashCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkIfMetadataShouldBeSet(
|
private boolean checkIfMetadataShouldBeSet(
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.SliderStrategy;
|
import org.schabi.newpipe.util.SliderStrategy;
|
||||||
|
|
||||||
public class PlaybackParameterDialog extends DialogFragment {
|
public class PlaybackParameterDialog extends DialogFragment {
|
||||||
|
@ -37,6 +39,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
|
|
||||||
private static final double DEFAULT_TEMPO = 1.00f;
|
private static final double DEFAULT_TEMPO = 1.00f;
|
||||||
private static final double DEFAULT_PITCH = 1.00f;
|
private static final double DEFAULT_PITCH = 1.00f;
|
||||||
|
private static final int DEFAULT_SEMITONES = 0;
|
||||||
private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
|
private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
|
||||||
private static final boolean DEFAULT_SKIP_SILENCE = false;
|
private static final boolean DEFAULT_SKIP_SILENCE = false;
|
||||||
|
|
||||||
|
@ -64,10 +67,11 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
|
|
||||||
private double initialTempo = DEFAULT_TEMPO;
|
private double initialTempo = DEFAULT_TEMPO;
|
||||||
private double initialPitch = DEFAULT_PITCH;
|
private double initialPitch = DEFAULT_PITCH;
|
||||||
|
private int initialSemitones = DEFAULT_SEMITONES;
|
||||||
private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
|
private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
|
||||||
private double tempo = DEFAULT_TEMPO;
|
private double tempo = DEFAULT_TEMPO;
|
||||||
private double pitch = DEFAULT_PITCH;
|
private double pitch = DEFAULT_PITCH;
|
||||||
private double stepSize = DEFAULT_STEP;
|
private int semitones = DEFAULT_SEMITONES;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private SeekBar tempoSlider;
|
private SeekBar tempoSlider;
|
||||||
|
@ -86,9 +90,19 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
@Nullable
|
@Nullable
|
||||||
private TextView pitchStepUpText;
|
private TextView pitchStepUpText;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
private SeekBar semitoneSlider;
|
||||||
|
@Nullable
|
||||||
|
private TextView semitoneCurrentText;
|
||||||
|
@Nullable
|
||||||
|
private TextView semitoneStepDownText;
|
||||||
|
@Nullable
|
||||||
|
private TextView semitoneStepUpText;
|
||||||
|
@Nullable
|
||||||
private CheckBox unhookingCheckbox;
|
private CheckBox unhookingCheckbox;
|
||||||
@Nullable
|
@Nullable
|
||||||
private CheckBox skipSilenceCheckbox;
|
private CheckBox skipSilenceCheckbox;
|
||||||
|
@Nullable
|
||||||
|
private CheckBox adjustBySemitonesCheckbox;
|
||||||
|
|
||||||
public static PlaybackParameterDialog newInstance(final double playbackTempo,
|
public static PlaybackParameterDialog newInstance(final double playbackTempo,
|
||||||
final double playbackPitch,
|
final double playbackPitch,
|
||||||
|
@ -101,6 +115,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
|
|
||||||
dialog.tempo = playbackTempo;
|
dialog.tempo = playbackTempo;
|
||||||
dialog.pitch = playbackPitch;
|
dialog.pitch = playbackPitch;
|
||||||
|
dialog.semitones = dialog.percentToSemitones(playbackPitch);
|
||||||
|
|
||||||
dialog.initialSkipSilence = playbackSkipSilence;
|
dialog.initialSkipSilence = playbackSkipSilence;
|
||||||
return dialog;
|
return dialog;
|
||||||
|
@ -111,7 +126,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
if (context instanceof Callback) {
|
if (context instanceof Callback) {
|
||||||
callback = (Callback) context;
|
callback = (Callback) context;
|
||||||
|
@ -127,22 +142,22 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
|
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
|
||||||
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
|
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
|
||||||
|
initialSemitones = percentToSemitones(initialPitch);
|
||||||
|
|
||||||
tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
|
tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
|
||||||
pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
|
pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
|
||||||
stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP);
|
semitones = percentToSemitones(pitch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
|
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
|
||||||
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
|
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
|
||||||
|
|
||||||
outState.putDouble(TEMPO_KEY, getCurrentTempo());
|
outState.putDouble(TEMPO_KEY, getCurrentTempo());
|
||||||
outState.putDouble(PITCH_KEY, getCurrentPitch());
|
outState.putDouble(PITCH_KEY, getCurrentPitch());
|
||||||
outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -160,9 +175,11 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||||
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
|
setPlaybackParameters(initialTempo, initialPitch,
|
||||||
|
initialSemitones, initialSkipSilence))
|
||||||
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
||||||
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
|
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH,
|
||||||
|
DEFAULT_SEMITONES, DEFAULT_SKIP_SILENCE))
|
||||||
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
|
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
|
||||||
setCurrentPlaybackParameters());
|
setCurrentPlaybackParameters());
|
||||||
|
|
||||||
|
@ -176,14 +193,49 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
private void setupControlViews(@NonNull final View rootView) {
|
private void setupControlViews(@NonNull final View rootView) {
|
||||||
setupHookingControl(rootView);
|
setupHookingControl(rootView);
|
||||||
setupSkipSilenceControl(rootView);
|
setupSkipSilenceControl(rootView);
|
||||||
|
setupAdjustBySemitonesControl(rootView);
|
||||||
|
|
||||||
setupTempoControl(rootView);
|
setupTempoControl(rootView);
|
||||||
setupPitchControl(rootView);
|
setupPitchControl(rootView);
|
||||||
|
setupSemitoneControl(rootView);
|
||||||
|
|
||||||
|
togglePitchSliderType(rootView);
|
||||||
|
|
||||||
setStepSize(stepSize);
|
|
||||||
setupStepSizeSelector(rootView);
|
setupStepSizeSelector(rootView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void togglePitchSliderType(@NonNull final View rootView) {
|
||||||
|
final RelativeLayout pitchControl = rootView.findViewById(R.id.pitchControl);
|
||||||
|
final RelativeLayout semitoneControl = rootView.findViewById(R.id.semitoneControl);
|
||||||
|
|
||||||
|
final View separatorStepSizeSelector =
|
||||||
|
rootView.findViewById(R.id.separatorStepSizeSelector);
|
||||||
|
final RelativeLayout.LayoutParams params =
|
||||||
|
(RelativeLayout.LayoutParams) separatorStepSizeSelector.getLayoutParams();
|
||||||
|
if (pitchControl != null && semitoneControl != null && unhookingCheckbox != null) {
|
||||||
|
if (getCurrentAdjustBySemitones()) {
|
||||||
|
// replaces pitchControl slider with semitoneControl slider
|
||||||
|
pitchControl.setVisibility(View.GONE);
|
||||||
|
semitoneControl.setVisibility(View.VISIBLE);
|
||||||
|
params.addRule(RelativeLayout.BELOW, R.id.semitoneControl);
|
||||||
|
|
||||||
|
// forces unhook for semitones
|
||||||
|
unhookingCheckbox.setChecked(true);
|
||||||
|
unhookingCheckbox.setEnabled(false);
|
||||||
|
|
||||||
|
setupTempoStepSizeSelector(rootView);
|
||||||
|
} else {
|
||||||
|
semitoneControl.setVisibility(View.GONE);
|
||||||
|
pitchControl.setVisibility(View.VISIBLE);
|
||||||
|
params.addRule(RelativeLayout.BELOW, R.id.pitchControl);
|
||||||
|
|
||||||
|
// (re)enables hooking selection
|
||||||
|
unhookingCheckbox.setEnabled(true);
|
||||||
|
setupCombinedStepSizeSelector(rootView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setupTempoControl(@NonNull final View rootView) {
|
private void setupTempoControl(@NonNull final View rootView) {
|
||||||
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
|
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
|
||||||
final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
|
final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
|
||||||
|
@ -234,23 +286,40 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupSemitoneControl(@NonNull final View rootView) {
|
||||||
|
semitoneSlider = rootView.findViewById(R.id.semitoneSeekbar);
|
||||||
|
semitoneCurrentText = rootView.findViewById(R.id.semitoneCurrentText);
|
||||||
|
semitoneStepDownText = rootView.findViewById(R.id.semitoneStepDown);
|
||||||
|
semitoneStepUpText = rootView.findViewById(R.id.semitoneStepUp);
|
||||||
|
|
||||||
|
if (semitoneCurrentText != null) {
|
||||||
|
semitoneCurrentText.setText(getSignedSemitonesString(semitones));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semitoneSlider != null) {
|
||||||
|
setSemitoneSlider(semitones);
|
||||||
|
semitoneSlider.setOnSeekBarChangeListener(getOnSemitoneChangedListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void setupHookingControl(@NonNull final View rootView) {
|
private void setupHookingControl(@NonNull final View rootView) {
|
||||||
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
||||||
if (unhookingCheckbox != null) {
|
if (unhookingCheckbox != null) {
|
||||||
// restore whether pitch and tempo are unhooked or not
|
// restores whether pitch and tempo are unhooked or not
|
||||||
unhookingCheckbox.setChecked(PreferenceManager
|
unhookingCheckbox.setChecked(PreferenceManager
|
||||||
.getDefaultSharedPreferences(requireContext())
|
.getDefaultSharedPreferences(requireContext())
|
||||||
.getBoolean(getString(R.string.playback_unhook_key), true));
|
.getBoolean(getString(R.string.playback_unhook_key), true));
|
||||||
|
|
||||||
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||||
// save whether pitch and tempo are unhooked or not
|
// saves whether pitch and tempo are unhooked or not
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
.edit()
|
.edit()
|
||||||
.putBoolean(getString(R.string.playback_unhook_key), isChecked)
|
.putBoolean(getString(R.string.playback_unhook_key), isChecked)
|
||||||
.apply();
|
.apply();
|
||||||
|
|
||||||
if (!isChecked) {
|
if (!isChecked) {
|
||||||
// when unchecked, slide back to the minimum of current tempo or pitch
|
// when unchecked, slides back to the minimum of current tempo or pitch
|
||||||
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
|
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
|
||||||
setSliders(minimum);
|
setSliders(minimum);
|
||||||
setCurrentPlaybackParameters();
|
setCurrentPlaybackParameters();
|
||||||
|
@ -268,7 +337,51 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupAdjustBySemitonesControl(@NonNull final View rootView) {
|
||||||
|
adjustBySemitonesCheckbox = rootView.findViewById(R.id.adjustBySemitonesCheckbox);
|
||||||
|
if (adjustBySemitonesCheckbox != null) {
|
||||||
|
// restores whether semitone adjustment is used or not
|
||||||
|
adjustBySemitonesCheckbox.setChecked(PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(requireContext())
|
||||||
|
.getBoolean(getString(R.string.playback_adjust_by_semitones_key), true));
|
||||||
|
|
||||||
|
// stores whether semitone adjustment is used or not
|
||||||
|
adjustBySemitonesCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.edit()
|
||||||
|
.putBoolean(getString(R.string.playback_adjust_by_semitones_key), isChecked)
|
||||||
|
.apply();
|
||||||
|
togglePitchSliderType(rootView);
|
||||||
|
if (isChecked) {
|
||||||
|
setPlaybackParameters(
|
||||||
|
getCurrentTempo(),
|
||||||
|
getCurrentPitch(),
|
||||||
|
Integer.min(12,
|
||||||
|
Integer.max(-12, percentToSemitones(getCurrentPitch())
|
||||||
|
)),
|
||||||
|
getCurrentSkipSilence()
|
||||||
|
);
|
||||||
|
setSemitoneSlider(Integer.min(12,
|
||||||
|
Integer.max(-12, percentToSemitones(getCurrentPitch()))
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
setPlaybackParameters(
|
||||||
|
getCurrentTempo(),
|
||||||
|
semitonesToPercent(getCurrentSemitones()),
|
||||||
|
getCurrentSemitones(),
|
||||||
|
getCurrentSkipSilence()
|
||||||
|
);
|
||||||
|
setPitchSlider(semitonesToPercent(getCurrentSemitones()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setupStepSizeSelector(@NonNull final View rootView) {
|
private void setupStepSizeSelector(@NonNull final View rootView) {
|
||||||
|
setStepSize(PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(requireContext())
|
||||||
|
.getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP));
|
||||||
|
|
||||||
final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
|
final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
|
||||||
final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
|
final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
|
||||||
final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
|
final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
|
||||||
|
@ -310,8 +423,27 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupTempoStepSizeSelector(@NonNull final View rootView) {
|
||||||
|
final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type);
|
||||||
|
if (playbackStepTypeText != null) {
|
||||||
|
playbackStepTypeText.setText(R.string.playback_tempo_step);
|
||||||
|
}
|
||||||
|
setupStepSizeSelector(rootView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupCombinedStepSizeSelector(@NonNull final View rootView) {
|
||||||
|
final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type);
|
||||||
|
if (playbackStepTypeText != null) {
|
||||||
|
playbackStepTypeText.setText(R.string.playback_step);
|
||||||
|
}
|
||||||
|
setupStepSizeSelector(rootView);
|
||||||
|
}
|
||||||
|
|
||||||
private void setStepSize(final double stepSize) {
|
private void setStepSize(final double stepSize) {
|
||||||
this.stepSize = stepSize;
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.edit()
|
||||||
|
.putFloat(getString(R.string.adjustment_step_key), (float) stepSize)
|
||||||
|
.apply();
|
||||||
|
|
||||||
if (tempoStepUpText != null) {
|
if (tempoStepUpText != null) {
|
||||||
tempoStepUpText.setText(getStepUpPercentString(stepSize));
|
tempoStepUpText.setText(getStepUpPercentString(stepSize));
|
||||||
|
@ -344,16 +476,30 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
setCurrentPlaybackParameters();
|
setCurrentPlaybackParameters();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (semitoneStepDownText != null) {
|
||||||
|
semitoneStepDownText.setOnClickListener(view -> {
|
||||||
|
onSemitoneSliderUpdated(getCurrentSemitones() - 1);
|
||||||
|
setCurrentPlaybackParameters();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semitoneStepUpText != null) {
|
||||||
|
semitoneStepUpText.setOnClickListener(view -> {
|
||||||
|
onSemitoneSliderUpdated(getCurrentSemitones() + 1);
|
||||||
|
setCurrentPlaybackParameters();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Sliders
|
// Sliders
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
|
private SimpleOnSeekBarChangeListener getOnTempoChangedListener() {
|
||||||
return new SeekBar.OnSeekBarChangeListener() {
|
return new SimpleOnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress,
|
||||||
final boolean fromUser) {
|
final boolean fromUser) {
|
||||||
final double currentTempo = strategy.valueOf(progress);
|
final double currentTempo = strategy.valueOf(progress);
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
|
@ -361,23 +507,13 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
setCurrentPlaybackParameters();
|
setCurrentPlaybackParameters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStartTrackingTouch(final SeekBar seekBar) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
|
private SimpleOnSeekBarChangeListener getOnPitchChangedListener() {
|
||||||
return new SeekBar.OnSeekBarChangeListener() {
|
return new SimpleOnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress,
|
||||||
final boolean fromUser) {
|
final boolean fromUser) {
|
||||||
final double currentPitch = strategy.valueOf(progress);
|
final double currentPitch = strategy.valueOf(progress);
|
||||||
if (fromUser) { // this change is first in chain
|
if (fromUser) { // this change is first in chain
|
||||||
|
@ -385,23 +521,27 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
setCurrentPlaybackParameters();
|
setCurrentPlaybackParameters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
@Override
|
|
||||||
public void onStartTrackingTouch(final SeekBar seekBar) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SimpleOnSeekBarChangeListener getOnSemitoneChangedListener() {
|
||||||
|
return new SimpleOnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress,
|
||||||
// Do Nothing.
|
final boolean fromUser) {
|
||||||
|
// semitone slider supplies values 0 to 24, subtraction by 12 is required
|
||||||
|
final int currentSemitones = progress - 12;
|
||||||
|
if (fromUser) { // this change is first in chain
|
||||||
|
onSemitoneSliderUpdated(currentSemitones);
|
||||||
|
// line below also saves semitones as pitch percentages
|
||||||
|
onPitchSliderUpdated(semitonesToPercent(currentSemitones));
|
||||||
|
setCurrentPlaybackParameters();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTempoSliderUpdated(final double newTempo) {
|
private void onTempoSliderUpdated(final double newTempo) {
|
||||||
if (unhookingCheckbox == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!unhookingCheckbox.isChecked()) {
|
if (!unhookingCheckbox.isChecked()) {
|
||||||
setSliders(newTempo);
|
setSliders(newTempo);
|
||||||
} else {
|
} else {
|
||||||
|
@ -410,9 +550,6 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPitchSliderUpdated(final double newPitch) {
|
private void onPitchSliderUpdated(final double newPitch) {
|
||||||
if (unhookingCheckbox == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!unhookingCheckbox.isChecked()) {
|
if (!unhookingCheckbox.isChecked()) {
|
||||||
setSliders(newPitch);
|
setSliders(newPitch);
|
||||||
} else {
|
} else {
|
||||||
|
@ -420,6 +557,10 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onSemitoneSliderUpdated(final int newSemitone) {
|
||||||
|
setSemitoneSlider(newSemitone);
|
||||||
|
}
|
||||||
|
|
||||||
private void setSliders(final double newValue) {
|
private void setSliders(final double newValue) {
|
||||||
setTempoSlider(newValue);
|
setTempoSlider(newValue);
|
||||||
setPitchSlider(newValue);
|
setPitchSlider(newValue);
|
||||||
|
@ -439,25 +580,49 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
pitchSlider.setProgress(strategy.progressOf(newPitch));
|
pitchSlider.setProgress(strategy.progressOf(newPitch));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setSemitoneSlider(final int newSemitone) {
|
||||||
|
if (semitoneSlider == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
semitoneSlider.setProgress(newSemitone + 12);
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Helper
|
// Helper
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void setCurrentPlaybackParameters() {
|
private void setCurrentPlaybackParameters() {
|
||||||
setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence());
|
if (getCurrentAdjustBySemitones()) {
|
||||||
|
setPlaybackParameters(
|
||||||
|
getCurrentTempo(),
|
||||||
|
semitonesToPercent(getCurrentSemitones()),
|
||||||
|
getCurrentSemitones(),
|
||||||
|
getCurrentSkipSilence()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setPlaybackParameters(
|
||||||
|
getCurrentTempo(),
|
||||||
|
getCurrentPitch(),
|
||||||
|
percentToSemitones(getCurrentPitch()),
|
||||||
|
getCurrentSkipSilence()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPlaybackParameters(final double newTempo, final double newPitch,
|
private void setPlaybackParameters(final double newTempo, final double newPitch,
|
||||||
final boolean skipSilence) {
|
final int newSemitones, final boolean skipSilence) {
|
||||||
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
|
if (callback != null && tempoCurrentText != null
|
||||||
|
&& pitchCurrentText != null && semitoneCurrentText != null) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Setting playback parameters to "
|
Log.d(TAG, "Setting playback parameters to "
|
||||||
+ "tempo=[" + newTempo + "], "
|
+ "tempo=[" + newTempo + "], "
|
||||||
+ "pitch=[" + newPitch + "]");
|
+ "pitch=[" + newPitch + "], "
|
||||||
|
+ "semitones=[" + newSemitones + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo));
|
tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo));
|
||||||
pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch));
|
pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch));
|
||||||
|
semitoneCurrentText.setText(getSignedSemitonesString(newSemitones));
|
||||||
callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence);
|
callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -470,14 +635,19 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress());
|
return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getCurrentStepSize() {
|
private int getCurrentSemitones() {
|
||||||
return stepSize;
|
// semitoneSlider is absolute, that's why - 12
|
||||||
|
return semitoneSlider == null ? semitones : semitoneSlider.getProgress() - 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getCurrentSkipSilence() {
|
private boolean getCurrentSkipSilence() {
|
||||||
return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
|
return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean getCurrentAdjustBySemitones() {
|
||||||
|
return adjustBySemitonesCheckbox != null && adjustBySemitonesCheckbox.isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static String getStepUpPercentString(final double percent) {
|
private static String getStepUpPercentString(final double percent) {
|
||||||
return STEP_UP_SIGN + getPercentString(percent);
|
return STEP_UP_SIGN + getPercentString(percent);
|
||||||
|
@ -493,8 +663,21 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
return PlayerHelper.formatPitch(percent);
|
return PlayerHelper.formatPitch(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String getSignedSemitonesString(final int semitones) {
|
||||||
|
return semitones > 0 ? "+" + semitones : "" + semitones;
|
||||||
|
}
|
||||||
|
|
||||||
public interface Callback {
|
public interface Callback {
|
||||||
void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
|
void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
|
||||||
boolean playbackSkipSilence);
|
boolean playbackSkipSilence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double semitonesToPercent(final int inSemitones) {
|
||||||
|
return Math.pow(2, inSemitones / 12.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int percentToSemitones(final double inPercent) {
|
||||||
|
return (int) Math.round(12 * Math.log(inPercent) / Math.log(2));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,11 +31,13 @@ public class PlayerDataSource {
|
||||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
private final int continueLoadingCheckIntervalBytes;
|
||||||
private final DataSource.Factory cacheDataSourceFactory;
|
private final DataSource.Factory cacheDataSourceFactory;
|
||||||
private final DataSource.Factory cachelessDataSourceFactory;
|
private final DataSource.Factory cachelessDataSourceFactory;
|
||||||
|
|
||||||
public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent,
|
public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent,
|
||||||
@NonNull final TransferListener transferListener) {
|
@NonNull final TransferListener transferListener) {
|
||||||
|
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
|
||||||
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
||||||
cachelessDataSourceFactory
|
cachelessDataSourceFactory
|
||||||
= new DefaultDataSourceFactory(context, userAgent, transferListener);
|
= new DefaultDataSourceFactory(context, userAgent, transferListener);
|
||||||
|
@ -91,6 +93,7 @@ public class PlayerDataSource {
|
||||||
|
|
||||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
||||||
|
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes)
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(
|
||||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import androidx.preference.PreferenceManager;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
@ -391,6 +392,19 @@ public final class PlayerHelper {
|
||||||
context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0;
|
context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getProgressiveLoadIntervalBytes(@NonNull final Context context) {
|
||||||
|
final String preferredIntervalBytes = getPreferences(context).getString(
|
||||||
|
context.getString(R.string.progressive_load_interval_key),
|
||||||
|
context.getString(R.string.progressive_load_interval_default_value));
|
||||||
|
|
||||||
|
if (context.getString(R.string.progressive_load_interval_default_value)
|
||||||
|
.equals(preferredIntervalBytes)) {
|
||||||
|
return ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||||
|
}
|
||||||
|
// Keeping the same KiB unit used by ProgressiveMediaSource
|
||||||
|
return Integer.parseInt(preferredIntervalBytes) * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Private helpers
|
// Private helpers
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.schabi.newpipe.player.listeners.view
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.player.Player
|
||||||
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click listener for the playbackSpeed textview of the player
|
||||||
|
*/
|
||||||
|
class PlaybackSpeedClickListener(
|
||||||
|
private val player: Player,
|
||||||
|
private val playbackSpeedPopupMenu: PopupMenu
|
||||||
|
) : View.OnClickListener {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG: String = "PlaybSpeedClickListener"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "onPlaybackSpeedClicked() called")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.videoPlayerSelected()) {
|
||||||
|
PlaybackParameterDialog.newInstance(
|
||||||
|
player.playbackSpeed.toDouble(),
|
||||||
|
player.playbackPitch.toDouble(),
|
||||||
|
player.playbackSkipSilence
|
||||||
|
) { speed: Float, pitch: Float, skipSilence: Boolean ->
|
||||||
|
player.setPlaybackParameters(
|
||||||
|
speed,
|
||||||
|
pitch,
|
||||||
|
skipSilence
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.show(player.parentActivity!!.supportFragmentManager, null)
|
||||||
|
} else {
|
||||||
|
playbackSpeedPopupMenu.show()
|
||||||
|
player.isSomePopupMenuVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
player.manageControlsAfterOnClick(v)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.schabi.newpipe.player.listeners.view
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.extractor.MediaFormat
|
||||||
|
import org.schabi.newpipe.player.Player
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click listener for the qualityTextView of the player
|
||||||
|
*/
|
||||||
|
class QualityClickListener(
|
||||||
|
private val player: Player,
|
||||||
|
private val qualityPopupMenu: PopupMenu
|
||||||
|
) : View.OnClickListener {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG: String = "QualityClickListener"
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n") // we don't need I18N because of a " "
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "onQualitySelectorClicked() called")
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityPopupMenu.show()
|
||||||
|
player.isSomePopupMenuVisible = true
|
||||||
|
|
||||||
|
val videoStream = player.selectedVideoStream
|
||||||
|
if (videoStream != null) {
|
||||||
|
player.binding.qualityTextView.text =
|
||||||
|
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
player.saveWasPlaying()
|
||||||
|
player.manageControlsAfterOnClick(v)
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,6 +88,8 @@ public class PlayerMediaSession implements MediaSessionCallback {
|
||||||
@Override
|
@Override
|
||||||
public void play() {
|
public void play() {
|
||||||
player.play();
|
player.play();
|
||||||
|
// hide the player controls even if the play command came from the media session
|
||||||
|
player.hideControls(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -4,20 +4,19 @@ import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.SingleObserver;
|
import io.reactivex.rxjava3.core.SingleObserver;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> extends PlayQueue {
|
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
|
extends PlayQueue {
|
||||||
boolean isInitial;
|
boolean isInitial;
|
||||||
private boolean isComplete;
|
private boolean isComplete;
|
||||||
|
|
||||||
|
@ -27,12 +26,15 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
|
||||||
|
|
||||||
private transient Disposable fetchReactor;
|
private transient Disposable fetchReactor;
|
||||||
|
|
||||||
AbstractInfoPlayQueue(final U item) {
|
protected AbstractInfoPlayQueue(final T info) {
|
||||||
this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0);
|
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractInfoPlayQueue(final int serviceId, final String url, final Page nextPage,
|
protected AbstractInfoPlayQueue(final int serviceId,
|
||||||
final List<StreamInfoItem> streams, final int index) {
|
final String url,
|
||||||
|
final Page nextPage,
|
||||||
|
final List<StreamInfoItem> streams,
|
||||||
|
final int index) {
|
||||||
super(index, extractListItems(streams));
|
super(index, extractListItems(streams));
|
||||||
|
|
||||||
this.baseUrl = url;
|
this.baseUrl = url;
|
||||||
|
@ -51,7 +53,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleObserver<T> getHeadListObserver() {
|
SingleObserver<T> getHeadListObserver() {
|
||||||
return new SingleObserver<T>() {
|
return new SingleObserver<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull final Disposable d) {
|
public void onSubscribe(@NonNull final Disposable d) {
|
||||||
if (isComplete || !isInitial || (fetchReactor != null
|
if (isComplete || !isInitial || (fetchReactor != null
|
||||||
|
@ -85,8 +87,8 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleObserver<ListExtractor.InfoItemsPage> getNextPageObserver() {
|
SingleObserver<ListExtractor.InfoItemsPage<StreamInfoItem>> getNextPageObserver() {
|
||||||
return new SingleObserver<ListExtractor.InfoItemsPage>() {
|
return new SingleObserver<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull final Disposable d) {
|
public void onSubscribe(@NonNull final Disposable d) {
|
||||||
if (isComplete || isInitial || (fetchReactor != null
|
if (isComplete || isInitial || (fetchReactor != null
|
||||||
|
@ -98,7 +100,8 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(@NonNull final ListExtractor.InfoItemsPage result) {
|
public void onSuccess(
|
||||||
|
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
|
||||||
if (!result.hasNextPage()) {
|
if (!result.hasNextPage()) {
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
}
|
}
|
||||||
|
@ -129,12 +132,6 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infoItems) {
|
private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infoItems) {
|
||||||
final List<PlayQueueItem> result = new ArrayList<>();
|
return infoItems.stream().map(PlayQueueItem::new).collect(Collectors.toList());
|
||||||
for (final InfoItem stream : infoItems) {
|
|
||||||
if (stream instanceof StreamInfoItem) {
|
|
||||||
result.add(new PlayQueueItem((StreamInfoItem) stream));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe.player.playqueue;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
|
@ -12,13 +11,10 @@ import java.util.List;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, ChannelInfoItem> {
|
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo> {
|
||||||
public ChannelPlayQueue(final ChannelInfoItem item) {
|
|
||||||
super(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelPlayQueue(final ChannelInfo info) {
|
public ChannelPlayQueue(final ChannelInfo info) {
|
||||||
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
|
super(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelPlayQueue(final int serviceId,
|
public ChannelPlayQueue(final int serviceId,
|
||||||
|
|
|
@ -528,7 +528,19 @@ public abstract class PlayQueue implements Serializable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final PlayQueue other = (PlayQueue) obj;
|
final PlayQueue other = (PlayQueue) obj;
|
||||||
return streams.equals(other.streams);
|
if (size() != other.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < size(); i++) {
|
||||||
|
final PlayQueueItem stream = streams.get(i);
|
||||||
|
final PlayQueueItem otherStream = other.streams.get(i);
|
||||||
|
// Check is based on serviceId and URL
|
||||||
|
if (stream.getServiceId() != otherStream.getServiceId()
|
||||||
|
|| !stream.getUrl().equals(otherStream.getUrl())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -52,6 +52,7 @@ public class PlayQueueItem implements Serializable {
|
||||||
item.getUploaderUrl(), item.getStreamType());
|
item.getUploaderUrl(), item.getStreamType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ParameterNumber")
|
||||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||||
final int serviceId, final long duration,
|
final int serviceId, final long duration,
|
||||||
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.player.playqueue;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
|
@ -11,13 +10,10 @@ import java.util.List;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo, PlaylistInfoItem> {
|
public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo> {
|
||||||
public PlaylistPlayQueue(final PlaylistInfoItem item) {
|
|
||||||
super(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlaylistPlayQueue(final PlaylistInfo info) {
|
public PlaylistPlayQueue(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
|
super(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaylistPlayQueue(final int serviceId,
|
public PlaylistPlayQueue(final int serviceId,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||||
|
|
||||||
|
@ -31,10 +32,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
private final PlayerDataSource dataSource;
|
private final PlayerDataSource dataSource;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final QualityResolver qualityResolver;
|
private final QualityResolver qualityResolver;
|
||||||
|
private SourceType streamSourceType;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String playbackQuality;
|
private String playbackQuality;
|
||||||
|
|
||||||
|
public enum SourceType {
|
||||||
|
LIVE_STREAM,
|
||||||
|
VIDEO_WITH_SEPARATED_AUDIO,
|
||||||
|
VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||||
|
}
|
||||||
|
|
||||||
public VideoPlaybackResolver(@NonNull final Context context,
|
public VideoPlaybackResolver(@NonNull final Context context,
|
||||||
@NonNull final PlayerDataSource dataSource,
|
@NonNull final PlayerDataSource dataSource,
|
||||||
@NonNull final QualityResolver qualityResolver) {
|
@NonNull final QualityResolver qualityResolver) {
|
||||||
|
@ -48,6 +56,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||||
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
||||||
if (liveSource != null) {
|
if (liveSource != null) {
|
||||||
|
streamSourceType = SourceType.LIVE_STREAM;
|
||||||
return liveSource;
|
return liveSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +64,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
|
|
||||||
// Create video stream source
|
// Create video stream source
|
||||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
|
||||||
final int index;
|
final int index;
|
||||||
if (videos.isEmpty()) {
|
if (videos.isEmpty()) {
|
||||||
index = -1;
|
index = -1;
|
||||||
|
@ -85,6 +94,9 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
PlayerHelper.cacheKeyOf(info, audio),
|
PlayerHelper.cacheKeyOf(info, audio),
|
||||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
||||||
mediaSources.add(audioSource);
|
mediaSources.add(audioSource);
|
||||||
|
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
||||||
|
} else {
|
||||||
|
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no audio or video sources, then this media source cannot be played back
|
// If there is no audio or video sources, then this media source cannot be played back
|
||||||
|
@ -118,6 +130,16 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last resolved {@link StreamInfo}'s {@link SourceType source type}.
|
||||||
|
*
|
||||||
|
* @return {@link Optional#empty()} if nothing was resolved, otherwise the {@link SourceType}
|
||||||
|
* of the last resolved {@link StreamInfo} inside an {@link Optional}
|
||||||
|
*/
|
||||||
|
public Optional<SourceType> getStreamSourceType() {
|
||||||
|
return Optional.ofNullable(streamSourceType);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getPlaybackQuality() {
|
public String getPlaybackQuality() {
|
||||||
return playbackQuality;
|
return playbackQuality;
|
||||||
|
|
|
@ -50,7 +50,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
public boolean onPreferenceTreeClick(final Preference preference) {
|
||||||
if (preference.getKey().equals(getString(R.string.caption_settings_key))) {
|
if (getString(R.string.caption_settings_key).equals(preference.getKey())) {
|
||||||
try {
|
try {
|
||||||
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
|
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
|
||||||
} catch (final ActivityNotFoundException e) {
|
} catch (final ActivityNotFoundException e) {
|
||||||
|
|
|
@ -185,7 +185,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
public boolean onPreferenceTreeClick(@NonNull final Preference preference) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onPreferenceTreeClick() called with: "
|
Log.d(TAG, "onPreferenceTreeClick() called with: "
|
||||||
+ "preference = [" + preference + "]");
|
+ "preference = [" + preference + "]");
|
||||||
|
|
|
@ -7,10 +7,9 @@ import android.view.MenuItem;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.CheckForNewAppVersion;
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||||
|
|
||||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
@ -24,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
|
||||||
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
||||||
|
|
||||||
// Check if the app is updatable
|
// Check if the app is updatable
|
||||||
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
|
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||||
getPreferenceScreen().removePreference(
|
getPreferenceScreen().removePreference(
|
||||||
findPreference(getString(R.string.update_pref_screen_key)));
|
findPreference(getString(R.string.update_pref_screen_key)));
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ import android.widget.Toast;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView;
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
@ -51,8 +50,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class PeertubeInstanceListFragment extends Fragment {
|
public class PeertubeInstanceListFragment extends Fragment {
|
||||||
private static final int MENU_ITEM_RESTORE_ID = 123456;
|
|
||||||
|
|
||||||
private final List<PeertubeInstance> instanceList = new ArrayList<>();
|
private final List<PeertubeInstance> instanceList = new ArrayList<>();
|
||||||
private PeertubeInstance selectedInstance;
|
private PeertubeInstance selectedInstance;
|
||||||
private String savedInstanceListKey;
|
private String savedInstanceListKey;
|
||||||
|
@ -142,17 +139,12 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
@NonNull final MenuInflater inflater) {
|
@NonNull final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.menu_chooser_fragment, menu);
|
||||||
final MenuItem restoreItem = menu
|
|
||||||
.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults);
|
|
||||||
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
||||||
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(),
|
|
||||||
R.drawable.ic_settings_backup_restore));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
|
if (item.getItemId() == R.id.menu_item_restore_default) {
|
||||||
restoreDefaults();
|
restoreDefaults();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -191,7 +183,7 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||||
.setTitle(R.string.restore_defaults)
|
.setTitle(R.string.restore_defaults)
|
||||||
.setMessage(R.string.restore_defaults_confirmation)
|
.setMessage(R.string.restore_defaults_confirmation)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||||
sharedPreferences.edit().remove(savedInstanceListKey).apply();
|
sharedPreferences.edit().remove(savedInstanceListKey).apply();
|
||||||
selectInstance(PeertubeInstance.defaultInstance);
|
selectInstance(PeertubeInstance.defaultInstance);
|
||||||
updateInstanceList();
|
updateInstanceList();
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -24,7 +25,6 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
import de.hdodenhof.circleimageview.CircleImageView;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observer;
|
import io.reactivex.rxjava3.core.Observer;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
@ -200,7 +200,7 @@ public class SelectChannelFragment extends DialogFragment {
|
||||||
|
|
||||||
public class SelectChannelItemHolder extends RecyclerView.ViewHolder {
|
public class SelectChannelItemHolder extends RecyclerView.ViewHolder {
|
||||||
public final View view;
|
public final View view;
|
||||||
final CircleImageView thumbnailView;
|
final ImageView thumbnailView;
|
||||||
final TextView titleView;
|
final TextView titleView;
|
||||||
SelectChannelItemHolder(final View v) {
|
SelectChannelItemHolder(final View v) {
|
||||||
super(v);
|
super(v);
|
||||||
|
|
|
@ -23,8 +23,6 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
import com.jakewharton.rxbinding4.widget.RxTextView;
|
import com.jakewharton.rxbinding4.widget.RxTextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.CheckForNewAppVersion;
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.SettingsLayoutBinding;
|
import org.schabi.newpipe.databinding.SettingsLayoutBinding;
|
||||||
|
@ -37,6 +35,7 @@ import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListen
|
||||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher;
|
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.KeyboardUtil;
|
import org.schabi.newpipe.util.KeyboardUtil;
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
|
@ -170,7 +169,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller,
|
public boolean onPreferenceStartFragment(@NonNull final PreferenceFragmentCompat caller,
|
||||||
final Preference preference) {
|
final Preference preference) {
|
||||||
showSettingsFragment(instantiateFragment(preference.getFragment()));
|
showSettingsFragment(instantiateFragment(preference.getFragment()));
|
||||||
return true;
|
return true;
|
||||||
|
@ -267,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
||||||
*/
|
*/
|
||||||
private void ensureSearchRepresentsApplicationState() {
|
private void ensureSearchRepresentsApplicationState() {
|
||||||
// Check if the update settings are available
|
// Check if the update settings are available
|
||||||
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
|
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||||
SettingsResourceRegistry.getInstance()
|
SettingsResourceRegistry.getInstance()
|
||||||
.getEntryByPreferencesResId(R.xml.update_settings)
|
.getEntryByPreferencesResId(R.xml.update_settings)
|
||||||
.setSearchable(false);
|
.setSearchable(false);
|
||||||
|
|
|
@ -116,6 +116,7 @@ public final class SettingsResourceRegistry {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public Class<? extends Fragment> getFragmentClass() {
|
public Class<? extends Fragment> getFragmentClass() {
|
||||||
return fragmentClass;
|
return fragmentClass;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewVersionWorker;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
public class UpdateSettingsFragment extends BasePreferenceFragment {
|
public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||||
|
@ -33,7 +32,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||||
// Reset the expire time. This is necessary to check for an update immediately.
|
// Reset the expire time. This is necessary to check for an update immediately.
|
||||||
defaultPreferences.edit()
|
defaultPreferences.edit()
|
||||||
.putLong(getString(R.string.update_expiry_key), 0).apply();
|
.putLong(getString(R.string.update_expiry_key), 0).apply();
|
||||||
startNewVersionCheckService();
|
NewVersionWorker.enqueueNewVersionCheckingWork(getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -14,10 +14,10 @@ import org.schabi.newpipe.util.Localization
|
||||||
* If the entry values array have anything other than numbers in it, an exception will be raised.
|
* If the entry values array have anything other than numbers in it, an exception will be raised.
|
||||||
*/
|
*/
|
||||||
class DurationListPreference : ListPreference {
|
class DurationListPreference : ListPreference {
|
||||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context?) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
override fun onAttached() {
|
override fun onAttached() {
|
||||||
super.onAttached()
|
super.onAttached()
|
||||||
|
|
|
@ -50,7 +50,7 @@ public class NotificationActionsPreference extends Preference {
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(final PreferenceViewHolder holder) {
|
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
|
||||||
super.onBindViewHolder(holder);
|
super.onBindViewHolder(holder);
|
||||||
|
|
||||||
holder.itemView.setClickable(false);
|
holder.itemView.setClickable(false);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.preference.PreferenceCategory;
|
||||||
import androidx.preference.PreferenceScreen;
|
import androidx.preference.PreferenceScreen;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -11,7 +12,7 @@ import java.util.stream.Stream;
|
||||||
public class PreferenceSearchConfiguration {
|
public class PreferenceSearchConfiguration {
|
||||||
private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction();
|
private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction();
|
||||||
|
|
||||||
private final List<String> parserIgnoreElements = Arrays.asList(
|
private final List<String> parserIgnoreElements = Collections.singletonList(
|
||||||
PreferenceCategory.class.getSimpleName());
|
PreferenceCategory.class.getSimpleName());
|
||||||
private final List<String> parserContainerElements = Arrays.asList(
|
private final List<String> parserContainerElements = Arrays.asList(
|
||||||
PreferenceCategory.class.getSimpleName(),
|
PreferenceCategory.class.getSimpleName(),
|
||||||
|
|
|
@ -58,22 +58,27 @@ public class PreferenceSearchItem {
|
||||||
this.searchIndexItemResId = searchIndexItemResId;
|
this.searchIndexItemResId = searchIndexItemResId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getSummary() {
|
public String getSummary() {
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getEntries() {
|
public String getEntries() {
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getBreadcrumbs() {
|
public String getBreadcrumbs() {
|
||||||
return breadcrumbs;
|
return breadcrumbs;
|
||||||
}
|
}
|
||||||
|
@ -94,7 +99,7 @@ public class PreferenceSearchItem {
|
||||||
getBreadcrumbs());
|
getBreadcrumbs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "PreferenceItem: " + title + " " + summary + " " + key;
|
return "PreferenceItem: " + title + " " + summary + " " + key;
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -17,7 +16,6 @@ import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView;
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
@ -107,12 +105,8 @@ public class ChooseTabsFragment extends Fragment {
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
@NonNull final MenuInflater inflater) {
|
@NonNull final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.menu_chooser_fragment, menu);
|
||||||
final MenuItem restoreItem = menu.add(R.string.restore_defaults);
|
menu.findItem(R.id.menu_item_restore_default).setOnMenuItemClickListener(item -> {
|
||||||
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
||||||
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(),
|
|
||||||
R.drawable.ic_settings_backup_restore));
|
|
||||||
restoreItem.setOnMenuItemClickListener(ev -> {
|
|
||||||
restoreDefaults();
|
restoreDefaults();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
@ -136,7 +130,7 @@ public class ChooseTabsFragment extends Fragment {
|
||||||
.setTitle(R.string.restore_defaults)
|
.setTitle(R.string.restore_defaults)
|
||||||
.setMessage(R.string.restore_defaults_confirmation)
|
.setMessage(R.string.restore_defaults_confirmation)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||||
tabsManager.resetTabs();
|
tabsManager.resetTabs();
|
||||||
updateTabList();
|
updateTabList();
|
||||||
selectedTabsAdapter.notifyDataSetChanged();
|
selectedTabsAdapter.notifyDataSetChanged();
|
||||||
|
|
|
@ -8,7 +8,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
import com.grack.nanojson.JsonSink;
|
import com.grack.nanojson.JsonStringWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem.LocalItemType;
|
import org.schabi.newpipe.database.LocalItem.LocalItemType;
|
||||||
|
@ -132,7 +132,7 @@ public abstract class Tab {
|
||||||
// JSON Handling
|
// JSON Handling
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void writeJsonOn(final JsonSink jsonSink) {
|
public void writeJsonOn(final JsonStringWriter jsonSink) {
|
||||||
jsonSink.object();
|
jsonSink.object();
|
||||||
|
|
||||||
jsonSink.value(JSON_TAB_ID_KEY, getTabId());
|
jsonSink.value(JSON_TAB_ID_KEY, getTabId());
|
||||||
|
@ -141,7 +141,7 @@ public abstract class Tab {
|
||||||
jsonSink.end();
|
jsonSink.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void writeDataToJson(final JsonSink writerSink) {
|
protected void writeDataToJson(final JsonStringWriter writerSink) {
|
||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ public abstract class Tab {
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
@Override
|
@Override
|
||||||
public int getTabIconRes(final Context context) {
|
public int getTabIconRes(final Context context) {
|
||||||
final int kioskIcon = KioskTranslator.getKioskIcon(kioskId, context);
|
final int kioskIcon = KioskTranslator.getKioskIcon(kioskId);
|
||||||
|
|
||||||
if (kioskIcon <= 0) {
|
if (kioskIcon <= 0) {
|
||||||
throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
|
throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
|
||||||
|
@ -355,7 +355,7 @@ public abstract class Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeDataToJson(final JsonSink writerSink) {
|
protected void writeDataToJson(final JsonStringWriter writerSink) {
|
||||||
writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId)
|
writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId)
|
||||||
.value(JSON_KIOSK_ID_KEY, kioskId);
|
.value(JSON_KIOSK_ID_KEY, kioskId);
|
||||||
}
|
}
|
||||||
|
@ -437,7 +437,7 @@ public abstract class Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeDataToJson(final JsonSink writerSink) {
|
protected void writeDataToJson(final JsonStringWriter writerSink) {
|
||||||
writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId)
|
writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId)
|
||||||
.value(JSON_CHANNEL_URL_KEY, channelUrl)
|
.value(JSON_CHANNEL_URL_KEY, channelUrl)
|
||||||
.value(JSON_CHANNEL_NAME_KEY, channelName);
|
.value(JSON_CHANNEL_NAME_KEY, channelName);
|
||||||
|
@ -496,7 +496,7 @@ public abstract class Tab {
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
@Override
|
@Override
|
||||||
public int getTabIconRes(final Context context) {
|
public int getTabIconRes(final Context context) {
|
||||||
return KioskTranslator.getKioskIcon(getDefaultKioskId(context), context);
|
return KioskTranslator.getKioskIcon(getDefaultKioskId(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -584,7 +584,7 @@ public abstract class Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeDataToJson(final JsonSink writerSink) {
|
protected void writeDataToJson(final JsonStringWriter writerSink) {
|
||||||
writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId)
|
writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId)
|
||||||
.value(JSON_PLAYLIST_URL_KEY, playlistUrl)
|
.value(JSON_PLAYLIST_URL_KEY, playlistUrl)
|
||||||
.value(JSON_PLAYLIST_NAME_KEY, playlistName)
|
.value(JSON_PLAYLIST_NAME_KEY, playlistName)
|
||||||
|
|
|
@ -142,6 +142,7 @@ public class Mp4FromDashWriter {
|
||||||
outStream = null;
|
outStream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("MethodLength")
|
||||||
public void build(final SharpStream output) throws IOException {
|
public void build(final SharpStream output) throws IOException {
|
||||||
if (done) {
|
if (done) {
|
||||||
throw new RuntimeException("already done");
|
throw new RuntimeException("already done");
|
||||||
|
|
|
@ -121,6 +121,7 @@ public class WebMWriter implements Closeable {
|
||||||
clustersOffsetsSizes = null;
|
clustersOffsetsSizes = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("MethodLength")
|
||||||
public void build(final SharpStream out) throws IOException, RuntimeException {
|
public void build(final SharpStream out) throws IOException, RuntimeException {
|
||||||
if (!out.canRewind()) {
|
if (!out.canRewind()) {
|
||||||
throw new IOException("The output stream must be allow seek");
|
throw new IOException("The output stream must be allow seek");
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public final class BitmapUtils {
|
|
||||||
private BitmapUtils() { }
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static Bitmap centerCrop(final Bitmap inputBitmap, final int newWidth,
|
|
||||||
final int newHeight) {
|
|
||||||
if (inputBitmap == null || inputBitmap.isRecycled()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final float sourceWidth = inputBitmap.getWidth();
|
|
||||||
final float sourceHeight = inputBitmap.getHeight();
|
|
||||||
|
|
||||||
final float xScale = newWidth / sourceWidth;
|
|
||||||
final float yScale = newHeight / sourceHeight;
|
|
||||||
|
|
||||||
final float newXScale;
|
|
||||||
final float newYScale;
|
|
||||||
|
|
||||||
if (yScale > xScale) {
|
|
||||||
newXScale = xScale / yScale;
|
|
||||||
newYScale = 1.0f;
|
|
||||||
} else {
|
|
||||||
newXScale = 1.0f;
|
|
||||||
newYScale = yScale / xScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
final float scaledWidth = newXScale * sourceWidth;
|
|
||||||
final float scaledHeight = newYScale * sourceHeight;
|
|
||||||
|
|
||||||
final int left = (int) ((sourceWidth - scaledWidth) / 2);
|
|
||||||
final int top = (int) ((sourceHeight - scaledHeight) / 2);
|
|
||||||
final int width = (int) scaledWidth;
|
|
||||||
final int height = (int) scaledHeight;
|
|
||||||
|
|
||||||
return Bitmap.createBitmap(inputBitmap, left, top, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,6 +19,8 @@
|
||||||
|
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -30,7 +32,6 @@ import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
|
||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
|
@ -41,6 +42,7 @@ import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo;
|
import org.schabi.newpipe.extractor.feed.FeedInfo;
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||||
|
@ -49,6 +51,7 @@ import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||||
|
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -57,8 +60,6 @@ import io.reactivex.rxjava3.core.Maybe;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
|
|
||||||
public final class ExtractorHelper {
|
public final class ExtractorHelper {
|
||||||
private static final String TAG = ExtractorHelper.class.getSimpleName();
|
private static final String TAG = ExtractorHelper.class.getSimpleName();
|
||||||
private static final InfoCache CACHE = InfoCache.getInstance();
|
private static final InfoCache CACHE = InfoCache.getInstance();
|
||||||
|
@ -84,7 +85,8 @@ public final class ExtractorHelper {
|
||||||
.fromQuery(searchString, contentFilter, sortFilter)));
|
.fromQuery(searchString, contentFilter, sortFilter)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage> getMoreSearchItems(final int serviceId,
|
public static Single<InfoItemsPage<InfoItem>> getMoreSearchItems(
|
||||||
|
final int serviceId,
|
||||||
final String searchString,
|
final String searchString,
|
||||||
final List<String> contentFilter,
|
final List<String> contentFilter,
|
||||||
final String sortFilter,
|
final String sortFilter,
|
||||||
|
@ -124,7 +126,8 @@ public final class ExtractorHelper {
|
||||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage> getMoreChannelItems(final int serviceId, final String url,
|
public static Single<InfoItemsPage<StreamInfoItem>> getMoreChannelItems(final int serviceId,
|
||||||
|
final String url,
|
||||||
final Page nextPage) {
|
final Page nextPage) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Single.fromCallable(() ->
|
return Single.fromCallable(() ->
|
||||||
|
@ -155,7 +158,8 @@ public final class ExtractorHelper {
|
||||||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
|
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
||||||
|
final int serviceId,
|
||||||
final CommentsInfo info,
|
final CommentsInfo info,
|
||||||
final Page nextPage) {
|
final Page nextPage) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
|
@ -163,7 +167,8 @@ public final class ExtractorHelper {
|
||||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId, final String url,
|
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||||
|
final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
||||||
|
@ -171,7 +176,8 @@ public final class ExtractorHelper {
|
||||||
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage> getMorePlaylistItems(final int serviceId, final String url,
|
public static Single<InfoItemsPage<StreamInfoItem>> getMorePlaylistItems(final int serviceId,
|
||||||
|
final String url,
|
||||||
final Page nextPage) {
|
final Page nextPage) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Single.fromCallable(() ->
|
return Single.fromCallable(() ->
|
||||||
|
@ -184,7 +190,8 @@ public final class ExtractorHelper {
|
||||||
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage> getMoreKioskItems(final int serviceId, final String url,
|
public static Single<InfoItemsPage<StreamInfoItem>> getMoreKioskItems(final int serviceId,
|
||||||
|
final String url,
|
||||||
final Page nextPage) {
|
final Page nextPage) {
|
||||||
return Single.fromCallable(() ->
|
return Single.fromCallable(() ->
|
||||||
KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||||
|
|
|
@ -57,7 +57,7 @@ public final class KioskTranslator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getKioskIcon(final String kioskId, final Context c) {
|
public static int getKioskIcon(final String kioskId) {
|
||||||
switch (kioskId) {
|
switch (kioskId) {
|
||||||
case "Trending":
|
case "Trending":
|
||||||
case "Top 50":
|
case "Top 50":
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.PointF;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class LayoutManagerSmoothScroller extends LinearLayoutManager {
|
|
||||||
public LayoutManagerSmoothScroller(final Context context) {
|
|
||||||
super(context, VERTICAL, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LayoutManagerSmoothScroller(final Context context, final int orientation,
|
|
||||||
final boolean reverseLayout) {
|
|
||||||
super(context, orientation, reverseLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void smoothScrollToPosition(final RecyclerView recyclerView,
|
|
||||||
final RecyclerView.State state, final int position) {
|
|
||||||
final RecyclerView.SmoothScroller smoothScroller
|
|
||||||
= new TopSnappedSmoothScroller(recyclerView.getContext());
|
|
||||||
smoothScroller.setTargetPosition(position);
|
|
||||||
startSmoothScroll(smoothScroller);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
|
|
||||||
TopSnappedSmoothScroller(final Context context) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PointF computeScrollVectorForPosition(final int targetPosition) {
|
|
||||||
return LayoutManagerSmoothScroller.this
|
|
||||||
.computeScrollVectorForPosition(targetPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int getVerticalSnapPreference() {
|
|
||||||
return SNAP_TO_START;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
@ -19,7 +20,11 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class ListHelper {
|
public final class ListHelper {
|
||||||
// Video format in order of quality. 0=lowest quality, n=highest quality
|
// Video format in order of quality. 0=lowest quality, n=highest quality
|
||||||
|
@ -33,8 +38,9 @@ public final class ListHelper {
|
||||||
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
||||||
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
||||||
|
|
||||||
private static final List<String> HIGH_RESOLUTION_LIST
|
private static final Set<String> HIGH_RESOLUTION_LIST
|
||||||
= Arrays.asList("1440p", "2160p", "1440p60", "2160p60");
|
// Uses a HashSet for better performance
|
||||||
|
= new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
|
||||||
|
|
||||||
private ListHelper() { }
|
private ListHelper() { }
|
||||||
|
|
||||||
|
@ -108,17 +114,21 @@ public final class ListHelper {
|
||||||
* Join the two lists of video streams (video_only and normal videos),
|
* Join the two lists of video streams (video_only and normal videos),
|
||||||
* and sort them according with default format chosen by the user.
|
* and sort them according with default format chosen by the user.
|
||||||
*
|
*
|
||||||
* @param context context to search for the format to give preference
|
* @param context the context to search for the format to give preference
|
||||||
* @param videoStreams normal videos list
|
* @param videoStreams the normal videos list
|
||||||
* @param videoOnlyStreams video only stream list
|
* @param videoOnlyStreams the video-only stream list
|
||||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||||
|
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||||
|
* streams and normal video streams are available
|
||||||
* @return the sorted list
|
* @return the sorted list
|
||||||
*/
|
*/
|
||||||
public static List<VideoStream> getSortedStreamVideosList(final Context context,
|
@NonNull
|
||||||
final List<VideoStream> videoStreams,
|
public static List<VideoStream> getSortedStreamVideosList(
|
||||||
final List<VideoStream>
|
@NonNull final Context context,
|
||||||
videoOnlyStreams,
|
@Nullable final List<VideoStream> videoStreams,
|
||||||
final boolean ascendingOrder) {
|
@Nullable final List<VideoStream> videoOnlyStreams,
|
||||||
|
final boolean ascendingOrder,
|
||||||
|
final boolean preferVideoOnlyStreams) {
|
||||||
final SharedPreferences preferences
|
final SharedPreferences preferences
|
||||||
= PreferenceManager.getDefaultSharedPreferences(context);
|
= PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
@ -128,7 +138,7 @@ public final class ListHelper {
|
||||||
R.string.default_video_format_key, R.string.default_video_format_value);
|
R.string.default_video_format_key, R.string.default_video_format_value);
|
||||||
|
|
||||||
return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams,
|
return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams,
|
||||||
videoOnlyStreams, ascendingOrder);
|
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -197,51 +207,50 @@ public final class ListHelper {
|
||||||
* @param videoStreams normal videos list
|
* @param videoStreams normal videos list
|
||||||
* @param videoOnlyStreams video only stream list
|
* @param videoOnlyStreams video only stream list
|
||||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||||
|
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||||
|
* streams and normal video streams are available
|
||||||
* @return the sorted list
|
* @return the sorted list
|
||||||
*/
|
*/
|
||||||
static List<VideoStream> getSortedStreamVideosList(final MediaFormat defaultFormat,
|
@NonNull
|
||||||
|
static List<VideoStream> getSortedStreamVideosList(
|
||||||
|
@Nullable final MediaFormat defaultFormat,
|
||||||
final boolean showHigherResolutions,
|
final boolean showHigherResolutions,
|
||||||
final List<VideoStream> videoStreams,
|
@Nullable final List<VideoStream> videoStreams,
|
||||||
final List<VideoStream> videoOnlyStreams,
|
@Nullable final List<VideoStream> videoOnlyStreams,
|
||||||
final boolean ascendingOrder) {
|
final boolean ascendingOrder,
|
||||||
final ArrayList<VideoStream> retList = new ArrayList<>();
|
final boolean preferVideoOnlyStreams
|
||||||
|
) {
|
||||||
|
// Determine order of streams
|
||||||
|
// The last added list is preferred
|
||||||
|
final List<List<VideoStream>> videoStreamsOrdered =
|
||||||
|
preferVideoOnlyStreams
|
||||||
|
? Arrays.asList(videoStreams, videoOnlyStreams)
|
||||||
|
: Arrays.asList(videoOnlyStreams, videoStreams);
|
||||||
|
|
||||||
|
final List<VideoStream> allInitialStreams = videoStreamsOrdered.stream()
|
||||||
|
// Ignore lists that are null
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
||||||
|
.filter(stream -> showHigherResolutions
|
||||||
|
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||||
|
|
||||||
if (videoOnlyStreams != null) {
|
|
||||||
for (final VideoStream stream : videoOnlyStreams) {
|
|
||||||
if (!showHigherResolutions
|
|
||||||
&& HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
retList.add(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (videoStreams != null) {
|
|
||||||
for (final VideoStream stream : videoStreams) {
|
|
||||||
if (!showHigherResolutions
|
|
||||||
&& HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
retList.add(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all to the hashmap
|
// Add all to the hashmap
|
||||||
for (final VideoStream videoStream : retList) {
|
for (final VideoStream videoStream : allInitialStreams) {
|
||||||
hashMap.put(videoStream.getResolution(), videoStream);
|
hashMap.put(videoStream.getResolution(), videoStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the values when the key == resolution, with the defaultFormat
|
// Override the values when the key == resolution, with the defaultFormat
|
||||||
for (final VideoStream videoStream : retList) {
|
for (final VideoStream videoStream : allInitialStreams) {
|
||||||
if (videoStream.getFormat() == defaultFormat) {
|
if (videoStream.getFormat() == defaultFormat) {
|
||||||
hashMap.put(videoStream.getResolution(), videoStream);
|
hashMap.put(videoStream.getResolution(), videoStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
retList.clear();
|
// Return the sorted list
|
||||||
retList.addAll(hashMap.values());
|
return sortStreamList(new ArrayList<>(hashMap.values()), ascendingOrder);
|
||||||
sortStreamList(retList, ascendingOrder);
|
|
||||||
return retList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -262,11 +271,13 @@ public final class ListHelper {
|
||||||
*
|
*
|
||||||
* @param videoStreams list that the sorting will be applied
|
* @param videoStreams list that the sorting will be applied
|
||||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||||
|
* @return The sorted list (same reference as parameter videoStreams)
|
||||||
*/
|
*/
|
||||||
private static void sortStreamList(final List<VideoStream> videoStreams,
|
private static List<VideoStream> sortStreamList(final List<VideoStream> videoStreams,
|
||||||
final boolean ascendingOrder) {
|
final boolean ascendingOrder) {
|
||||||
final Comparator<VideoStream> comparator = ListHelper::compareVideoStreamResolution;
|
final Comparator<VideoStream> comparator = ListHelper::compareVideoStreamResolution;
|
||||||
Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed());
|
Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed());
|
||||||
|
return videoStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -277,28 +288,12 @@ public final class ListHelper {
|
||||||
* @param audioStreams List of audio streams
|
* @param audioStreams List of audio streams
|
||||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
* @return Index of audio stream that produces the most compact results or -1 if not found
|
||||||
*/
|
*/
|
||||||
static int getHighestQualityAudioIndex(@Nullable MediaFormat format,
|
static int getHighestQualityAudioIndex(@Nullable final MediaFormat format,
|
||||||
final List<AudioStream> audioStreams) {
|
@Nullable final List<AudioStream> audioStreams) {
|
||||||
int result = -1;
|
return getAudioIndexByHighestRank(format, audioStreams,
|
||||||
if (audioStreams != null) {
|
// Compares descending (last = highest rank)
|
||||||
while (result == -1) {
|
(s1, s2) -> compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_QUALITY_RANKING)
|
||||||
AudioStream prevStream = null;
|
);
|
||||||
for (int idx = 0; idx < audioStreams.size(); idx++) {
|
|
||||||
final AudioStream stream = audioStreams.get(idx);
|
|
||||||
if ((format == null || stream.getFormat() == format)
|
|
||||||
&& (prevStream == null || compareAudioStreamBitrate(prevStream, stream,
|
|
||||||
AUDIO_FORMAT_QUALITY_RANKING) < 0)) {
|
|
||||||
prevStream = stream;
|
|
||||||
result = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result == -1 && format == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
format = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -309,28 +304,47 @@ public final class ListHelper {
|
||||||
* @param audioStreams List of audio streams
|
* @param audioStreams List of audio streams
|
||||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
* @return Index of audio stream that produces the most compact results or -1 if not found
|
||||||
*/
|
*/
|
||||||
static int getMostCompactAudioIndex(@Nullable MediaFormat format,
|
static int getMostCompactAudioIndex(@Nullable final MediaFormat format,
|
||||||
final List<AudioStream> audioStreams) {
|
@Nullable final List<AudioStream> audioStreams) {
|
||||||
int result = -1;
|
|
||||||
if (audioStreams != null) {
|
return getAudioIndexByHighestRank(format, audioStreams,
|
||||||
while (result == -1) {
|
// The "-" is important -> Compares ascending (first = highest rank)
|
||||||
AudioStream prevStream = null;
|
(s1, s2) -> -compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_EFFICIENCY_RANKING)
|
||||||
for (int idx = 0; idx < audioStreams.size(); idx++) {
|
);
|
||||||
final AudioStream stream = audioStreams.get(idx);
|
|
||||||
if ((format == null || stream.getFormat() == format)
|
|
||||||
&& (prevStream == null || compareAudioStreamBitrate(prevStream, stream,
|
|
||||||
AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) {
|
|
||||||
prevStream = stream;
|
|
||||||
result = idx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the audio-stream from the list with the highest rank, depending on the comparator.
|
||||||
|
* Format will be ignored if it yields no results.
|
||||||
|
*
|
||||||
|
* @param targetedFormat The target format type or null if it doesn't matter
|
||||||
|
* @param audioStreams List of audio streams
|
||||||
|
* @param comparator The comparator used for determining the max/best/highest ranked value
|
||||||
|
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
||||||
|
*/
|
||||||
|
private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat,
|
||||||
|
@Nullable final List<AudioStream> audioStreams,
|
||||||
|
final Comparator<AudioStream> comparator) {
|
||||||
|
if (audioStreams == null || audioStreams.isEmpty()) {
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
if (result == -1 && format == null) {
|
|
||||||
break;
|
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
||||||
|
.filter(audioStream -> targetedFormat == null
|
||||||
|
|| audioStream.getFormat() == targetedFormat)
|
||||||
|
.max(comparator)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (highestRankedAudioStream == null) {
|
||||||
|
// Fallback: Ignore targetedFormat if not null
|
||||||
|
if (targetedFormat != null) {
|
||||||
|
return getAudioIndexByHighestRank(null, audioStreams, comparator);
|
||||||
}
|
}
|
||||||
format = null;
|
// targetedFormat is already null -> return -1
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return result;
|
return audioStreams.indexOf(highestRankedAudioStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
|
@ -214,7 +215,8 @@ public final class NavigationHelper {
|
||||||
// External Players
|
// External Players
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static void playOnExternalAudioPlayer(final Context context, final StreamInfo info) {
|
public static void playOnExternalAudioPlayer(@NonNull final Context context,
|
||||||
|
@NonNull final StreamInfo info) {
|
||||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
||||||
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
|
@ -226,9 +228,11 @@ public final class NavigationHelper {
|
||||||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
|
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void playOnExternalVideoPlayer(final Context context, final StreamInfo info) {
|
public static void playOnExternalVideoPlayer(@NonNull final Context context,
|
||||||
|
@NonNull final StreamInfo info) {
|
||||||
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
|
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
|
||||||
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false));
|
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
|
||||||
|
false));
|
||||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
|
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
|
||||||
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
|
@ -240,8 +244,10 @@ public final class NavigationHelper {
|
||||||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
|
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void playOnExternalPlayer(final Context context, final String name,
|
public static void playOnExternalPlayer(@NonNull final Context context,
|
||||||
final String artist, final Stream stream) {
|
@Nullable final String name,
|
||||||
|
@Nullable final String artist,
|
||||||
|
@NonNull final Stream stream) {
|
||||||
final Intent intent = new Intent();
|
final Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
|
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
|
||||||
|
@ -253,7 +259,8 @@ public final class NavigationHelper {
|
||||||
resolveActivityOrAskToInstall(context, intent);
|
resolveActivityOrAskToInstall(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) {
|
public static void resolveActivityOrAskToInstall(@NonNull final Context context,
|
||||||
|
@NonNull final Intent intent) {
|
||||||
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||||
ShareUtils.openIntentInApp(context, intent, false);
|
ShareUtils.openIntentInApp(context, intent, false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -402,6 +409,15 @@ public final class NavigationHelper {
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openChannelFragment(@NonNull final Fragment fragment,
|
||||||
|
@NonNull final StreamInfoItem item,
|
||||||
|
final String uploaderUrl) {
|
||||||
|
// For some reason `getParentFragmentManager()` doesn't work, but this does.
|
||||||
|
openChannelFragment(
|
||||||
|
fragment.requireActivity().getSupportFragmentManager(),
|
||||||
|
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
||||||
|
}
|
||||||
|
|
||||||
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
||||||
final int serviceId, final String url,
|
final int serviceId, final String url,
|
||||||
@NonNull final String name) {
|
@NonNull final String name) {
|
||||||
|
|
116
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
116
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.Signature
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import org.schabi.newpipe.App
|
||||||
|
import org.schabi.newpipe.error.ErrorInfo
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||||
|
import org.schabi.newpipe.error.UserAction
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.cert.CertificateEncodingException
|
||||||
|
import java.security.cert.CertificateException
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
object ReleaseVersionUtil {
|
||||||
|
// Public key of the certificate that is used in NewPipe release versions
|
||||||
|
private const val 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"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isReleaseApk(): Boolean {
|
||||||
|
return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||||
|
*
|
||||||
|
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||||
|
*/
|
||||||
|
private val certificateSHA1Fingerprint: String
|
||||||
|
get() {
|
||||||
|
val app = App.getApp()
|
||||||
|
val signatures: List<Signature> = try {
|
||||||
|
PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
showRequestError(app, e, "Could not find package info")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (signatures.isEmpty()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val x509cert = try {
|
||||||
|
val cert = signatures[0].toByteArray()
|
||||||
|
val input: InputStream = ByteArrayInputStream(cert)
|
||||||
|
val cf = CertificateFactory.getInstance("X509")
|
||||||
|
cf.generateCertificate(input) as X509Certificate
|
||||||
|
} catch (e: CertificateException) {
|
||||||
|
showRequestError(app, e, "Certificate error")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val md = MessageDigest.getInstance("SHA1")
|
||||||
|
val publicKey = md.digest(x509cert.encoded)
|
||||||
|
byte2HexFormatted(publicKey)
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||||
|
""
|
||||||
|
} catch (e: CertificateEncodingException) {
|
||||||
|
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun byte2HexFormatted(arr: ByteArray): String {
|
||||||
|
val str = StringBuilder(arr.size * 2)
|
||||||
|
for (i in arr.indices) {
|
||||||
|
var h = Integer.toHexString(arr[i].toInt())
|
||||||
|
val l = h.length
|
||||||
|
if (l == 1) {
|
||||||
|
h = "0$h"
|
||||||
|
}
|
||||||
|
if (l > 2) {
|
||||||
|
h = h.substring(l - 2, l)
|
||||||
|
}
|
||||||
|
str.append(h.uppercase())
|
||||||
|
if (i < arr.size - 1) {
|
||||||
|
str.append(':')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showRequestError(app: App, e: Exception, request: String) {
|
||||||
|
createNotification(
|
||||||
|
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
|
||||||
|
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce expiry date time in between 6 hours and 72 hours from now
|
||||||
|
*
|
||||||
|
* @return Epoch second of expiry date time
|
||||||
|
*/
|
||||||
|
fun coerceUpdateCheckExpiry(expiryString: String?): Long {
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
return expiryString?.let {
|
||||||
|
var expiry =
|
||||||
|
ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
||||||
|
expiry = maxOf(expiry, now.plusHours(6))
|
||||||
|
expiry = minOf(expiry, now.plusHours(72))
|
||||||
|
expiry.toEpochSecond()
|
||||||
|
} ?: now.plusHours(6).toEpochSecond()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for putting the uploader url into the database - when required.
|
|
||||||
*/
|
|
||||||
public final class SaveUploaderUrlHelper {
|
|
||||||
private SaveUploaderUrlHelper() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public functions which call the function that does
|
|
||||||
// the actual work with the correct parameters
|
|
||||||
public static void saveUploaderUrlIfNeeded(@NonNull final Fragment fragment,
|
|
||||||
@NonNull final StreamInfoItem infoItem,
|
|
||||||
@NonNull final SaveUploaderUrlCallback callback) {
|
|
||||||
saveUploaderUrlIfNeeded(fragment.requireContext(),
|
|
||||||
infoItem.getServiceId(),
|
|
||||||
infoItem.getUrl(),
|
|
||||||
infoItem.getUploaderUrl(),
|
|
||||||
callback);
|
|
||||||
}
|
|
||||||
public static void saveUploaderUrlIfNeeded(@NonNull final Context context,
|
|
||||||
@NonNull final PlayQueueItem queueItem,
|
|
||||||
@NonNull final SaveUploaderUrlCallback callback) {
|
|
||||||
saveUploaderUrlIfNeeded(context,
|
|
||||||
queueItem.getServiceId(),
|
|
||||||
queueItem.getUrl(),
|
|
||||||
queueItem.getUploaderUrl(),
|
|
||||||
callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches and saves the uploaderUrl if it is empty (meaning that it does
|
|
||||||
* not exist in the video item). The callback is called with either the
|
|
||||||
* fetched uploaderUrl, or the already saved uploaderUrl, but it is always
|
|
||||||
* called with a valid uploaderUrl that can be used to show channel details.
|
|
||||||
*
|
|
||||||
* @param context Context
|
|
||||||
* @param serviceId The serviceId of the item
|
|
||||||
* @param url The item url
|
|
||||||
* @param uploaderUrl The uploaderUrl of the item, if null or empty, it
|
|
||||||
* will be fetched using the item url.
|
|
||||||
* @param callback The callback that returns the fetched or existing
|
|
||||||
* uploaderUrl
|
|
||||||
*/
|
|
||||||
private static void saveUploaderUrlIfNeeded(@NonNull final Context context,
|
|
||||||
final int serviceId,
|
|
||||||
@NonNull final String url,
|
|
||||||
// Only used if not null or empty
|
|
||||||
@Nullable final String uploaderUrl,
|
|
||||||
@NonNull final SaveUploaderUrlCallback callback) {
|
|
||||||
if (isNullOrEmpty(uploaderUrl)) {
|
|
||||||
Toast.makeText(context, R.string.loading_channel_details,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(result -> {
|
|
||||||
NewPipeDatabase.getInstance(context).streamDAO()
|
|
||||||
.setUploaderUrl(serviceId, url, result.getUploaderUrl())
|
|
||||||
.subscribeOn(Schedulers.io()).subscribe();
|
|
||||||
callback.onCallback(result.getUploaderUrl());
|
|
||||||
}, throwable -> ErrorUtil.createNotification(context,
|
|
||||||
new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
|
||||||
"Could not load channel details")
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
callback.onCallback(uploaderUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface SaveUploaderUrlCallback {
|
|
||||||
void onCallback(@NonNull String uploaderUrl);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ public final class SerializedCache {
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
private static final SerializedCache INSTANCE = new SerializedCache();
|
private static final SerializedCache INSTANCE = new SerializedCache();
|
||||||
private static final int MAX_ITEMS_ON_CACHE = 5;
|
private static final int MAX_ITEMS_ON_CACHE = 5;
|
||||||
private static final LruCache<String, CacheData> LRU_CACHE =
|
private static final LruCache<String, CacheData<?>> LRU_CACHE =
|
||||||
new LruCache<>(MAX_ITEMS_ON_CACHE);
|
new LruCache<>(MAX_ITEMS_ON_CACHE);
|
||||||
private static final String TAG = "SerializedCache";
|
private static final String TAG = "SerializedCache";
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ public final class SerializedCache {
|
||||||
Log.d(TAG, "get() called with: key = [" + key + "]");
|
Log.d(TAG, "get() called with: key = [" + key + "]");
|
||||||
}
|
}
|
||||||
synchronized (LRU_CACHE) {
|
synchronized (LRU_CACHE) {
|
||||||
final CacheData data = LRU_CACHE.get(key);
|
final CacheData<?> data = LRU_CACHE.get(key);
|
||||||
return data != null ? getItem(data, type) : null;
|
return data != null ? getItem(data, type) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ public final class SerializedCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private <T> T getItem(@NonNull final CacheData data, @NonNull final Class<T> type) {
|
private <T> T getItem(@NonNull final CacheData<?> data, @NonNull final Class<T> type) {
|
||||||
return type.isAssignableFrom(data.type) ? type.cast(data.item) : null;
|
return type.isAssignableFrom(data.type) ? type.cast(data.item) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.widget.SeekBar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why the hell didn't they make a stub implementation for this?
|
||||||
|
*/
|
||||||
|
abstract class SimpleOnSeekBarChangeListener : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||||
|
}
|
128
app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
Normal file
128
app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for fetching additional data for stream items when needed.
|
||||||
|
*/
|
||||||
|
public final class SparseItemUtil {
|
||||||
|
private SparseItemUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to certainly obtain an single play queue with all of the data filled in when the
|
||||||
|
* stream info item you are handling might be sparse, e.g. because it was fetched via a {@link
|
||||||
|
* org.schabi.newpipe.extractor.feed.FeedExtractor}. FeedExtractors provide a fast and
|
||||||
|
* lightweight method to fetch info, but the info might be incomplete (see
|
||||||
|
* {@link org.schabi.newpipe.local.feed.service.FeedLoadService} for more details).
|
||||||
|
*
|
||||||
|
* @param context Android context
|
||||||
|
* @param item item which is checked and eventually loaded completely
|
||||||
|
* @param callback callback to call with the single play queue built from the original item if
|
||||||
|
* all info was available, otherwise from the fetched {@link
|
||||||
|
* org.schabi.newpipe.extractor.stream.StreamInfo}
|
||||||
|
*/
|
||||||
|
public static void fetchItemInfoIfSparse(@NonNull final Context context,
|
||||||
|
@NonNull final StreamInfoItem item,
|
||||||
|
@NonNull final Consumer<SinglePlayQueue> callback) {
|
||||||
|
if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM)
|
||||||
|
|| item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) {
|
||||||
|
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
|
||||||
|
// uploader url, probably all info is already there, so there is no need to fetch it
|
||||||
|
callback.accept(new SinglePlayQueue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// either the duration or the uploader url are not available, so fetch more info
|
||||||
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
|
streamInfo -> callback.accept(new SinglePlayQueue(streamInfo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to certainly obtain an uploader url when the stream info item or play queue item you
|
||||||
|
* are handling might not have the uploader url (e.g. because it was fetched with {@link
|
||||||
|
* org.schabi.newpipe.extractor.feed.FeedExtractor}). A toast is shown if loading details is
|
||||||
|
* required.
|
||||||
|
*
|
||||||
|
* @param context Android context
|
||||||
|
* @param serviceId serviceId of the item
|
||||||
|
* @param url item url
|
||||||
|
* @param uploaderUrl uploaderUrl of the item; if null or empty will be fetched
|
||||||
|
* @param callback callback to be called with either the original uploaderUrl, if it was a
|
||||||
|
* valid url, otherwise with the uploader url obtained by fetching the {@link
|
||||||
|
* org.schabi.newpipe.extractor.stream.StreamInfo} corresponding to the item
|
||||||
|
*/
|
||||||
|
public static void fetchUploaderUrlIfSparse(@NonNull final Context context,
|
||||||
|
final int serviceId,
|
||||||
|
@NonNull final String url,
|
||||||
|
@Nullable final String uploaderUrl,
|
||||||
|
@NonNull final Consumer<String> callback) {
|
||||||
|
if (isNullOrEmpty(uploaderUrl)) {
|
||||||
|
fetchStreamInfoAndSaveToDatabase(context, serviceId, url,
|
||||||
|
streamInfo -> callback.accept(streamInfo.getUploaderUrl()));
|
||||||
|
} else {
|
||||||
|
callback.accept(uploaderUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the stream info corresponding to the given data on an I/O thread, stores the result in
|
||||||
|
* the database and calls the callback on the main thread with the result. A toast will be shown
|
||||||
|
* to the user about loading stream details, so this needs to be called on the main thread.
|
||||||
|
*
|
||||||
|
* @param context Android context
|
||||||
|
* @param serviceId service id of the stream to load
|
||||||
|
* @param url url of the stream to load
|
||||||
|
* @param callback callback to be called with the result
|
||||||
|
*/
|
||||||
|
private static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context context,
|
||||||
|
final int serviceId,
|
||||||
|
@NonNull final String url,
|
||||||
|
final Consumer<StreamInfo> callback) {
|
||||||
|
Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show();
|
||||||
|
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
// save to database in the background (not on main thread)
|
||||||
|
Completable.fromAction(() -> NewPipeDatabase.getInstance(context)
|
||||||
|
.streamDAO().upsert(new StreamEntity(result)))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.doOnError(throwable ->
|
||||||
|
ErrorUtil.createNotification(context,
|
||||||
|
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||||
|
"Saving stream info to database", result)))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
// call callback on main thread with the obtained result
|
||||||
|
callback.accept(result);
|
||||||
|
}, throwable -> ErrorUtil.createNotification(context,
|
||||||
|
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||||
|
"Loading stream info: " + url, serviceId)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,238 +0,0 @@
|
||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public enum StreamDialogEntry {
|
|
||||||
//////////////////////////////////////
|
|
||||||
// enum values with DEFAULT actions //
|
|
||||||
//////////////////////////////////////
|
|
||||||
|
|
||||||
show_channel_details(R.string.show_channel_details, (fragment, item) -> {
|
|
||||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(fragment, item,
|
|
||||||
uploaderUrl -> openChannelFragment(fragment, item, uploaderUrl));
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueues the stream automatically to the current PlayerType.<br>
|
|
||||||
* <br>
|
|
||||||
* Info: Add this entry within showStreamDialog.
|
|
||||||
*/
|
|
||||||
enqueue(R.string.enqueue_stream, (fragment, item) -> {
|
|
||||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
|
||||||
NavigationHelper.enqueueOnPlayer(fragment.getContext(), fullItem));
|
|
||||||
}),
|
|
||||||
|
|
||||||
enqueue_next(R.string.enqueue_next_stream, (fragment, item) -> {
|
|
||||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
|
||||||
NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), fullItem));
|
|
||||||
}),
|
|
||||||
|
|
||||||
start_here_on_background(R.string.start_here_on_background, (fragment, item) -> {
|
|
||||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), fullItem, true));
|
|
||||||
}),
|
|
||||||
|
|
||||||
start_here_on_popup(R.string.start_here_on_popup, (fragment, item) -> {
|
|
||||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
|
||||||
NavigationHelper.playOnPopupPlayer(fragment.getContext(), fullItem, true));
|
|
||||||
}),
|
|
||||||
|
|
||||||
set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
|
|
||||||
}), // has to be set manually
|
|
||||||
|
|
||||||
delete(R.string.delete, (fragment, item) -> {
|
|
||||||
}), // has to be set manually
|
|
||||||
|
|
||||||
append_playlist(R.string.add_to_playlist, (fragment, item) -> {
|
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
|
||||||
fragment.getContext(),
|
|
||||||
Collections.singletonList(new StreamEntity(item)),
|
|
||||||
dialog -> dialog.show(
|
|
||||||
fragment.getParentFragmentManager(),
|
|
||||||
"StreamDialogEntry@"
|
|
||||||
+ (dialog instanceof PlaylistAppendDialog ? "append" : "create")
|
|
||||||
+ "_playlist"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
play_with_kodi(R.string.play_with_kodi_title, (fragment, item) -> {
|
|
||||||
final Uri videoUrl = Uri.parse(item.getUrl());
|
|
||||||
try {
|
|
||||||
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
|
|
||||||
} catch (final Exception e) {
|
|
||||||
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
share(R.string.share, (fragment, item) ->
|
|
||||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
|
||||||
item.getThumbnailUrl())),
|
|
||||||
|
|
||||||
open_in_browser(R.string.open_in_browser, (fragment, item) ->
|
|
||||||
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
|
|
||||||
|
|
||||||
|
|
||||||
mark_as_watched(R.string.mark_as_watched, (fragment, item) -> {
|
|
||||||
new HistoryRecordManager(fragment.getContext())
|
|
||||||
.markAsWatched(item)
|
|
||||||
.onErrorComplete()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
///////////////
|
|
||||||
// variables //
|
|
||||||
///////////////
|
|
||||||
|
|
||||||
private static StreamDialogEntry[] enabledEntries;
|
|
||||||
private final int resource;
|
|
||||||
private final StreamDialogEntryAction defaultAction;
|
|
||||||
private StreamDialogEntryAction customAction;
|
|
||||||
|
|
||||||
StreamDialogEntry(final int resource, final StreamDialogEntryAction defaultAction) {
|
|
||||||
this.resource = resource;
|
|
||||||
this.defaultAction = defaultAction;
|
|
||||||
this.customAction = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
// non-static methods to initialize and edit entries //
|
|
||||||
///////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public static void setEnabledEntries(final List<StreamDialogEntry> entries) {
|
|
||||||
setEnabledEntries(entries.toArray(new StreamDialogEntry[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To be called before using {@link #setCustomAction(StreamDialogEntryAction)}.
|
|
||||||
*
|
|
||||||
* @param entries the entries to be enabled
|
|
||||||
*/
|
|
||||||
public static void setEnabledEntries(final StreamDialogEntry... entries) {
|
|
||||||
// cleanup from last time StreamDialogEntry was used
|
|
||||||
for (final StreamDialogEntry streamDialogEntry : values()) {
|
|
||||||
streamDialogEntry.customAction = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
enabledEntries = entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] getCommands(final Context context) {
|
|
||||||
final String[] commands = new String[enabledEntries.length];
|
|
||||||
for (int i = 0; i != enabledEntries.length; ++i) {
|
|
||||||
commands[i] = context.getResources().getString(enabledEntries[i].resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
// static methods that act on enabled entries //
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
public static void clickOn(final int which, final Fragment fragment,
|
|
||||||
final StreamInfoItem infoItem) {
|
|
||||||
if (enabledEntries[which].customAction == null) {
|
|
||||||
enabledEntries[which].defaultAction.onClick(fragment, infoItem);
|
|
||||||
} else {
|
|
||||||
enabledEntries[which].customAction.onClick(fragment, infoItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called.
|
|
||||||
*
|
|
||||||
* @param action the action to be set
|
|
||||||
*/
|
|
||||||
public void setCustomAction(final StreamDialogEntryAction action) {
|
|
||||||
this.customAction = action;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface StreamDialogEntryAction {
|
|
||||||
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldAddMarkAsWatched(final StreamType streamType,
|
|
||||||
final Context context) {
|
|
||||||
final boolean isWatchHistoryEnabled = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context)
|
|
||||||
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
|
||||||
return streamType != StreamType.AUDIO_LIVE_STREAM
|
|
||||||
&& streamType != StreamType.LIVE_STREAM
|
|
||||||
&& isWatchHistoryEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
// private method to open channel fragment //
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
|
|
||||||
private static void openChannelFragment(final Fragment fragment,
|
|
||||||
final StreamInfoItem item,
|
|
||||||
final String uploaderUrl) {
|
|
||||||
// For some reason `getParentFragmentManager()` doesn't work, but this does.
|
|
||||||
NavigationHelper.openChannelFragment(
|
|
||||||
fragment.requireActivity().getSupportFragmentManager(),
|
|
||||||
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
// helper functions //
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
|
|
||||||
private static void fetchItemInfoIfSparse(final Fragment fragment,
|
|
||||||
final StreamInfoItem item,
|
|
||||||
final Consumer<SinglePlayQueue> callback) {
|
|
||||||
if (!(item.getStreamType() == StreamType.LIVE_STREAM
|
|
||||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM)
|
|
||||||
&& item.getDuration() < 0) {
|
|
||||||
// Sparse item: fetched by fast fetch
|
|
||||||
ExtractorHelper.getStreamInfo(
|
|
||||||
item.getServiceId(),
|
|
||||||
item.getUrl(),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(result -> {
|
|
||||||
final HistoryRecordManager recordManager =
|
|
||||||
new HistoryRecordManager(fragment.getContext());
|
|
||||||
recordManager.saveStreamState(result, 0)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnError(throwable -> Log.e("StreamDialogEntry",
|
|
||||||
throwable.toString()))
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
callback.accept(new SinglePlayQueue(result));
|
|
||||||
}, throwable -> Log.e("StreamDialogEntry", throwable.toString()));
|
|
||||||
} else {
|
|
||||||
callback.accept(new SinglePlayQueue(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,6 +12,7 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
|
@ -137,7 +138,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
||||||
final SecondaryStreamHelper secondary = secondaryStreams == null ? null
|
final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null
|
||||||
: secondaryStreams.get(position);
|
: secondaryStreams.get(position);
|
||||||
if (secondary != null) {
|
if (secondary != null) {
|
||||||
final long size
|
final long size
|
||||||
|
@ -153,16 +154,11 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
|
|
||||||
if (stream instanceof SubtitlesStream) {
|
if (stream instanceof SubtitlesStream) {
|
||||||
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
||||||
} else {
|
} else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||||
switch (stream.getFormat()) {
|
|
||||||
case WEBMA_OPUS:
|
|
||||||
// noinspection AndroidLintSetTextI18n
|
// noinspection AndroidLintSetTextI18n
|
||||||
formatNameView.setText("opus");
|
formatNameView.setText("opus");
|
||||||
break;
|
} else {
|
||||||
default:
|
|
||||||
formatNameView.setText(stream.getFormat().getName());
|
formatNameView.setText(stream.getFormat().getName());
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qualityView.setText(qualityString);
|
qualityView.setText(qualityString);
|
||||||
|
|
|
@ -10,6 +10,10 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util class that provides methods which are related to the Kodi Media Center and its Kore app.
|
||||||
|
* @see <a href="https://kodi.tv/">Kodi website</a>
|
||||||
|
*/
|
||||||
public final class KoreUtils {
|
public final class KoreUtils {
|
||||||
private KoreUtils() { }
|
private KoreUtils() { }
|
||||||
|
|
||||||
|
|
|
@ -1,303 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 Alexander Rvachev <rvacheva@nxt.ru>
|
|
||||||
* FocusOverlayView.java is part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.schabi.newpipe.views;
|
|
||||||
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.text.Layout;
|
|
||||||
import android.text.Selection;
|
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.ClickableSpan;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.ViewParent;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class LargeTextMovementMethod extends LinkMovementMethod {
|
|
||||||
private final Rect visibleRect = new Rect();
|
|
||||||
|
|
||||||
private int direction;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTakeFocus(final TextView view, final Spannable text, final int dir) {
|
|
||||||
Selection.removeSelection(text);
|
|
||||||
|
|
||||||
super.onTakeFocus(view, text, dir);
|
|
||||||
|
|
||||||
this.direction = dirToRelative(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean handleMovementKey(final TextView widget,
|
|
||||||
final Spannable buffer,
|
|
||||||
final int keyCode,
|
|
||||||
final int movementMetaState,
|
|
||||||
final KeyEvent event) {
|
|
||||||
if (!doHandleMovement(widget, buffer, keyCode, movementMetaState, event)) {
|
|
||||||
// clear selection to make sure, that it does not confuse focus handling code
|
|
||||||
Selection.removeSelection(buffer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean doHandleMovement(final TextView widget,
|
|
||||||
final Spannable buffer,
|
|
||||||
final int keyCode,
|
|
||||||
final int movementMetaState,
|
|
||||||
final KeyEvent event) {
|
|
||||||
final int newDir = keyToDir(keyCode);
|
|
||||||
|
|
||||||
if (direction != 0 && newDir != direction) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.direction = 0;
|
|
||||||
|
|
||||||
final ViewGroup root = findScrollableParent(widget);
|
|
||||||
|
|
||||||
widget.getHitRect(visibleRect);
|
|
||||||
|
|
||||||
root.offsetDescendantRectToMyCoords((View) widget.getParent(), visibleRect);
|
|
||||||
|
|
||||||
return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean up(final TextView widget, final Spannable buffer) {
|
|
||||||
if (gotoPrev(widget, buffer)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.up(widget, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean left(final TextView widget, final Spannable buffer) {
|
|
||||||
if (gotoPrev(widget, buffer)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.left(widget, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean right(final TextView widget, final Spannable buffer) {
|
|
||||||
if (gotoNext(widget, buffer)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.right(widget, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean down(final TextView widget, final Spannable buffer) {
|
|
||||||
if (gotoNext(widget, buffer)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.down(widget, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean gotoPrev(final TextView view, final Spannable buffer) {
|
|
||||||
final Layout layout = view.getLayout();
|
|
||||||
if (layout == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final View root = findScrollableParent(view);
|
|
||||||
|
|
||||||
final int rootHeight = root.getHeight();
|
|
||||||
|
|
||||||
if (visibleRect.top >= 0) {
|
|
||||||
// we fit entirely into the viewport, no need for fancy footwork
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int topExtra = -visibleRect.top;
|
|
||||||
|
|
||||||
final int firstVisibleLineNumber = layout.getLineForVertical(topExtra);
|
|
||||||
|
|
||||||
// when deciding whether to pass "focus" to span, account for one more line
|
|
||||||
// this ensures, that focus is never passed to spans partially outside scroll window
|
|
||||||
final int visibleStart = firstVisibleLineNumber == 0
|
|
||||||
? 0
|
|
||||||
: layout.getLineStart(firstVisibleLineNumber - 1);
|
|
||||||
|
|
||||||
final ClickableSpan[] candidates = buffer.getSpans(
|
|
||||||
visibleStart, buffer.length(), ClickableSpan.class);
|
|
||||||
|
|
||||||
if (candidates.length != 0) {
|
|
||||||
final int a = Selection.getSelectionStart(buffer);
|
|
||||||
final int b = Selection.getSelectionEnd(buffer);
|
|
||||||
|
|
||||||
final int selStart = Math.min(a, b);
|
|
||||||
final int selEnd = Math.max(a, b);
|
|
||||||
|
|
||||||
int bestStart = -1;
|
|
||||||
int bestEnd = -1;
|
|
||||||
|
|
||||||
for (final ClickableSpan candidate : candidates) {
|
|
||||||
final int start = buffer.getSpanStart(candidate);
|
|
||||||
final int end = buffer.getSpanEnd(candidate);
|
|
||||||
|
|
||||||
if ((end < selEnd || selStart == selEnd) && start >= visibleStart) {
|
|
||||||
if (end > bestEnd) {
|
|
||||||
bestStart = buffer.getSpanStart(candidate);
|
|
||||||
bestEnd = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestStart >= 0) {
|
|
||||||
Selection.setSelection(buffer, bestEnd, bestStart);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final float fourLines = view.getTextSize() * 4;
|
|
||||||
|
|
||||||
visibleRect.left = 0;
|
|
||||||
visibleRect.right = view.getWidth();
|
|
||||||
visibleRect.top = Math.max(0, (int) (topExtra - fourLines));
|
|
||||||
visibleRect.bottom = visibleRect.top + rootHeight;
|
|
||||||
|
|
||||||
return view.requestRectangleOnScreen(visibleRect);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean gotoNext(final TextView view, final Spannable buffer) {
|
|
||||||
final Layout layout = view.getLayout();
|
|
||||||
if (layout == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final View root = findScrollableParent(view);
|
|
||||||
|
|
||||||
final int rootHeight = root.getHeight();
|
|
||||||
|
|
||||||
if (visibleRect.bottom <= rootHeight) {
|
|
||||||
// we fit entirely into the viewport, no need for fancy footwork
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int bottomExtra = visibleRect.bottom - rootHeight;
|
|
||||||
|
|
||||||
final int visibleBottomBorder = view.getHeight() - bottomExtra;
|
|
||||||
|
|
||||||
final int lineCount = layout.getLineCount();
|
|
||||||
|
|
||||||
final int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder);
|
|
||||||
|
|
||||||
// when deciding whether to pass "focus" to span, account for one more line
|
|
||||||
// this ensures, that focus is never passed to spans partially outside scroll window
|
|
||||||
final int visibleEnd = lastVisibleLineNumber == lineCount - 1
|
|
||||||
? buffer.length()
|
|
||||||
: layout.getLineEnd(lastVisibleLineNumber - 1);
|
|
||||||
|
|
||||||
final ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class);
|
|
||||||
|
|
||||||
if (candidates.length != 0) {
|
|
||||||
final int a = Selection.getSelectionStart(buffer);
|
|
||||||
final int b = Selection.getSelectionEnd(buffer);
|
|
||||||
|
|
||||||
final int selStart = Math.min(a, b);
|
|
||||||
final int selEnd = Math.max(a, b);
|
|
||||||
|
|
||||||
int bestStart = Integer.MAX_VALUE;
|
|
||||||
int bestEnd = Integer.MAX_VALUE;
|
|
||||||
|
|
||||||
for (final ClickableSpan candidate : candidates) {
|
|
||||||
final int start = buffer.getSpanStart(candidate);
|
|
||||||
final int end = buffer.getSpanEnd(candidate);
|
|
||||||
|
|
||||||
if ((start > selStart || selStart == selEnd) && end <= visibleEnd) {
|
|
||||||
if (start < bestStart) {
|
|
||||||
bestStart = start;
|
|
||||||
bestEnd = buffer.getSpanEnd(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestEnd < Integer.MAX_VALUE) {
|
|
||||||
// cool, we have managed to find next link without having to adjust self within view
|
|
||||||
Selection.setSelection(buffer, bestStart, bestEnd);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// there are no links within visible area, but still some text past visible area
|
|
||||||
// scroll visible area further in required direction
|
|
||||||
final float fourLines = view.getTextSize() * 4;
|
|
||||||
|
|
||||||
visibleRect.left = 0;
|
|
||||||
visibleRect.right = view.getWidth();
|
|
||||||
visibleRect.bottom = Math.min((int) (visibleBottomBorder + fourLines), view.getHeight());
|
|
||||||
visibleRect.top = visibleRect.bottom - rootHeight;
|
|
||||||
|
|
||||||
return view.requestRectangleOnScreen(visibleRect);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ViewGroup findScrollableParent(final View view) {
|
|
||||||
View current = view;
|
|
||||||
|
|
||||||
ViewParent parent;
|
|
||||||
do {
|
|
||||||
parent = current.getParent();
|
|
||||||
|
|
||||||
if (parent == current || !(parent instanceof View)) {
|
|
||||||
return (ViewGroup) view.getRootView();
|
|
||||||
}
|
|
||||||
|
|
||||||
current = (View) parent;
|
|
||||||
|
|
||||||
if (current.isScrollContainer()) {
|
|
||||||
return (ViewGroup) current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int dirToRelative(final int dir) {
|
|
||||||
switch (dir) {
|
|
||||||
case View.FOCUS_DOWN:
|
|
||||||
case View.FOCUS_RIGHT:
|
|
||||||
return View.FOCUS_FORWARD;
|
|
||||||
case View.FOCUS_UP:
|
|
||||||
case View.FOCUS_LEFT:
|
|
||||||
return View.FOCUS_BACKWARD;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int keyToDir(final int keyCode) {
|
|
||||||
switch (keyCode) {
|
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
||||||
return View.FOCUS_BACKWARD;
|
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
||||||
return View.FOCUS_FORWARD;
|
|
||||||
}
|
|
||||||
|
|
||||||
return View.FOCUS_FORWARD;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
package org.schabi.newpipe.views.player
|
package org.schabi.newpipe.views.player
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.animation.addListener
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.PlayerFastSeekSecondsViewBinding
|
import org.schabi.newpipe.databinding.PlayerFastSeekSecondsViewBinding
|
||||||
import org.schabi.newpipe.util.DeviceUtils
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
|
@ -163,19 +163,10 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context
|
||||||
setFloatValues(0f, 1f)
|
setFloatValues(0f, 1f)
|
||||||
|
|
||||||
addUpdateListener { update(it.animatedValue as Float) }
|
addUpdateListener { update(it.animatedValue as Float) }
|
||||||
addListener(object : AnimatorListener {
|
addListener(
|
||||||
override fun onAnimationStart(animation: Animator?) {
|
onStart = { start() },
|
||||||
start()
|
onEnd = { end() }
|
||||||
}
|
)
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
|
||||||
end()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(animation: Animator?) = Unit
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animator?) = Unit
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,14 +192,7 @@ public class MissionsFragment extends Fragment {
|
||||||
updateList();
|
updateList();
|
||||||
return true;
|
return true;
|
||||||
case R.id.clear_list:
|
case R.id.clear_list:
|
||||||
AlertDialog.Builder prompt = new AlertDialog.Builder(mContext);
|
showClearDownloadHistoryPrompt();
|
||||||
prompt.setTitle(R.string.clear_download_history);
|
|
||||||
prompt.setMessage(R.string.confirm_prompt);
|
|
||||||
// Intentionally misusing button's purpose in order to achieve good order
|
|
||||||
prompt.setNegativeButton(R.string.clear_download_history, (dialog, which) -> mAdapter.clearFinishedDownloads(false));
|
|
||||||
prompt.setPositiveButton(R.string.delete_downloaded_files, (dialog, which) -> mAdapter.clearFinishedDownloads(true));
|
|
||||||
prompt.setNeutralButton(R.string.cancel, null);
|
|
||||||
prompt.create().show();
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.start_downloads:
|
case R.id.start_downloads:
|
||||||
mBinder.getDownloadManager().startAllMissions();
|
mBinder.getDownloadManager().startAllMissions();
|
||||||
|
@ -212,6 +205,32 @@ public class MissionsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showClearDownloadHistoryPrompt() {
|
||||||
|
// ask the user whether he wants to just clear history or instead delete files on disk
|
||||||
|
new AlertDialog.Builder(mContext)
|
||||||
|
.setTitle(R.string.clear_download_history)
|
||||||
|
.setMessage(R.string.confirm_prompt)
|
||||||
|
// Intentionally misusing buttons' purpose in order to achieve good order
|
||||||
|
.setNegativeButton(R.string.clear_download_history,
|
||||||
|
(dialog, which) -> mAdapter.clearFinishedDownloads(false))
|
||||||
|
.setNeutralButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.delete_downloaded_files,
|
||||||
|
(dialog, which) -> showDeleteDownloadedFilesConfirmationPrompt())
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showDeleteDownloadedFilesConfirmationPrompt() {
|
||||||
|
// make sure the user confirms once more before deleting files on disk
|
||||||
|
new AlertDialog.Builder(mContext)
|
||||||
|
.setTitle(R.string.delete_downloaded_files_confirm)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.ok,
|
||||||
|
(dialog, which) -> mAdapter.clearFinishedDownloads(true))
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateList() {
|
private void updateList() {
|
||||||
if (mLinear) {
|
if (mLinear) {
|
||||||
mList.setLayoutManager(mLinearManager);
|
mList.setLayoutManager(mLinearManager);
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
|
||||||
</vector>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M13,7h-2v4L7,11v2h4v4h2v-4h4v-2h-4L13,7zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
|
||||||
</vector>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
|
|
||||||
</vector>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
|
||||||
</vector>
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue