mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-08 16:30:34 +00:00
Merge branch 'refactor' into Video-description-compose
# Conflicts: # app/build.gradle # app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java # app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java # app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt # app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
This commit is contained in:
commit
cd96927358
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -47,10 +47,10 @@ jobs:
|
|||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
run: git checkout -B "$BRANCH"
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
@ -88,10 +88,10 @@ jobs:
|
|||||||
sudo udevadm control --reload-rules
|
sudo udevadm control --reload-rules
|
||||||
sudo udevadm trigger --name-match=kvm
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
@ -121,10 +121,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ captures/
|
|||||||
*.class
|
*.class
|
||||||
app/debug/
|
app/debug/
|
||||||
app/release/
|
app/release/
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# vscode / eclipse files
|
# vscode / eclipse files
|
||||||
*.classpath
|
*.classpath
|
||||||
|
@ -10,6 +10,7 @@ plugins {
|
|||||||
id "checkstyle"
|
id "checkstyle"
|
||||||
id "org.sonarqube" version "4.0.0.2929"
|
id "org.sonarqube" version "4.0.0.2929"
|
||||||
id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}"
|
id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}"
|
||||||
|
id 'com.google.dagger.hilt.android'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -94,6 +95,7 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
compose true
|
compose true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
@ -114,7 +116,7 @@ ext {
|
|||||||
androidxRoomVersion = '2.6.1'
|
androidxRoomVersion = '2.6.1'
|
||||||
androidxWorkVersion = '2.8.1'
|
androidxWorkVersion = '2.8.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
stateSaverVersion = '1.4.1'
|
||||||
exoPlayerVersion = '2.18.7'
|
exoPlayerVersion = '2.18.7'
|
||||||
googleAutoServiceVersion = '1.1.1'
|
googleAutoServiceVersion = '1.1.1'
|
||||||
groupieVersion = '2.10.1'
|
groupieVersion = '2.10.1'
|
||||||
@ -122,6 +124,8 @@ ext {
|
|||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
leakCanaryVersion = '2.12'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
|
|
||||||
|
coilVersion = '3.0.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@ -190,6 +194,10 @@ sonar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
||||||
@ -200,7 +208,8 @@ dependencies {
|
|||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.2'
|
// WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead
|
||||||
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
@ -236,8 +245,9 @@ dependencies {
|
|||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
implementation 'com.github.livefront:bridge:v2.0.2'
|
||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
implementation "com.evernote:android-state:$stateSaverVersion"
|
||||||
|
kapt "com.evernote:android-state-processor:$stateSaverVersion"
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.17.2"
|
implementation "org.jsoup:jsoup:1.17.2"
|
||||||
@ -264,7 +274,8 @@ dependencies {
|
|||||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation 'io.coil-kt:coil-compose:2.7.0'
|
implementation "io.coil-kt.coil3:coil-compose:${coilVersion}"
|
||||||
|
implementation "io.coil-kt.coil3:coil-network-okhttp:${coilVersion}"
|
||||||
|
|
||||||
// Markdown library for Android
|
// Markdown library for Android
|
||||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||||
@ -286,18 +297,29 @@ dependencies {
|
|||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
||||||
|
|
||||||
// Jetpack Compose
|
// Jetpack Compose
|
||||||
implementation(platform('androidx.compose:compose-bom:2024.09.00'))
|
implementation(platform('androidx.compose:compose-bom:2024.10.01'))
|
||||||
implementation 'androidx.compose.material3:material3'
|
implementation 'androidx.compose.material3:material3'
|
||||||
implementation 'androidx.compose.material3.adaptive:adaptive'
|
implementation 'androidx.compose.material3.adaptive:adaptive'
|
||||||
implementation 'androidx.activity:activity-compose'
|
implementation 'androidx.activity:activity-compose'
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose'
|
||||||
|
implementation 'androidx.compose.ui:ui-text' // Needed for parsing HTML to AnnotatedString
|
||||||
|
implementation 'androidx.compose.material:material-icons-extended'
|
||||||
|
|
||||||
|
// Jetpack Compose related dependencies
|
||||||
implementation 'androidx.paging:paging-compose:3.3.2'
|
implementation 'androidx.paging:paging-compose:3.3.2'
|
||||||
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
|
implementation "androidx.navigation:navigation-compose:2.8.3"
|
||||||
|
|
||||||
// Coroutines interop
|
// Coroutines interop
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
|
||||||
|
|
||||||
|
// Hilt
|
||||||
|
implementation("com.google.dagger:hilt-android:2.51.1")
|
||||||
|
kapt("com.google.dagger:hilt-compiler:2.51.1")
|
||||||
|
|
||||||
|
// Scroll
|
||||||
|
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||||
|
9
app/proguard-rules.pro
vendored
9
app/proguard-rules.pro
vendored
@ -13,15 +13,6 @@
|
|||||||
## Rules for ExoPlayer
|
## Rules for ExoPlayer
|
||||||
-keep class com.google.android.exoplayer2.** { *; }
|
-keep class com.google.android.exoplayer2.** { *; }
|
||||||
|
|
||||||
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
|
||||||
-dontwarn icepick.**
|
|
||||||
-keep class icepick.** { *; }
|
|
||||||
-keep class **$$Icepick { *; }
|
|
||||||
-keepclasseswithmembernames class * {
|
|
||||||
@icepick.* <fields>;
|
|
||||||
}
|
|
||||||
-keepnames class * { @icepick.State *;}
|
|
||||||
|
|
||||||
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
@ -77,6 +77,11 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsV2Activity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -1,275 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.NotificationChannelCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.config.CoreConfigurationBuilder;
|
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import coil.ImageLoader;
|
|
||||||
import coil.ImageLoaderFactory;
|
|
||||||
import coil.util.DebugLogger;
|
|
||||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
|
||||||
import io.reactivex.rxjava3.functions.Consumer;
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
|
||||||
* App.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe 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.
|
|
||||||
*
|
|
||||||
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class App extends Application implements ImageLoaderFactory {
|
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
|
||||||
private static final String TAG = App.class.toString();
|
|
||||||
|
|
||||||
private boolean isFirstRun = false;
|
|
||||||
private static App app;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static App getApp() {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
initACRA();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
app = this;
|
|
||||||
|
|
||||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
|
||||||
Log.i(TAG, "This is a phoenix process! "
|
|
||||||
+ "Aborting initialization of App[onCreate]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the last used preference version is set
|
|
||||||
// to determine whether this is the first app run
|
|
||||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
|
||||||
isFirstRun = lastUsedPrefVersion == -1;
|
|
||||||
|
|
||||||
// Initialize settings first because other initializations can use its values
|
|
||||||
NewPipeSettings.initSettings(this);
|
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
|
||||||
Localization.getPreferredLocalization(this),
|
|
||||||
Localization.getPreferredContentCountry(this));
|
|
||||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
|
||||||
|
|
||||||
StateSaver.init(this);
|
|
||||||
initNotificationChannels();
|
|
||||||
|
|
||||||
ServiceHelper.initServices(this);
|
|
||||||
|
|
||||||
// Initialize image loader
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
|
||||||
prefs.getString(getString(R.string.image_quality_key),
|
|
||||||
getString(R.string.image_quality_default))));
|
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ImageLoader newImageLoader() {
|
|
||||||
return new ImageLoader.Builder(this)
|
|
||||||
.allowRgb565(ContextCompat.getSystemService(this, ActivityManager.class)
|
|
||||||
.isLowRamDevice())
|
|
||||||
.logger(BuildConfig.DEBUG ? new DebugLogger() : null)
|
|
||||||
.crossfade(true)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
|
||||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
|
||||||
setCookiesToDownloader(downloader);
|
|
||||||
return downloader;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
getApplicationContext());
|
|
||||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
|
||||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
|
||||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureRxJavaErrorHandler() {
|
|
||||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull final Throwable throwable) {
|
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
|
||||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
|
||||||
|
|
||||||
final Throwable actualThrowable;
|
|
||||||
if (throwable instanceof UndeliverableException) {
|
|
||||||
// As UndeliverableException is a wrapper,
|
|
||||||
// get the cause of it to get the "real" exception
|
|
||||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
|
||||||
} else {
|
|
||||||
actualThrowable = throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Throwable> errors;
|
|
||||||
if (actualThrowable instanceof CompositeException) {
|
|
||||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
|
||||||
} else {
|
|
||||||
errors = List.of(actualThrowable);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
|
||||||
if (isThrowableIgnored(error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isThrowableCritical(error)) {
|
|
||||||
reportException(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
|
||||||
// When exception is not reported, log it
|
|
||||||
if (isDisposedRxExceptionsReported()) {
|
|
||||||
reportException(actualThrowable);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
|
||||||
// Don't crash the application over a simple network problem
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
// network api cancellation
|
|
||||||
IOException.class, SocketException.class,
|
|
||||||
// blocking code disposed
|
|
||||||
InterruptedException.class, InterruptedIOException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
|
||||||
// Though these exceptions cannot be ignored
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
|
||||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
|
||||||
IllegalStateException.class); // bug in operator
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportException(@NonNull final Throwable throwable) {
|
|
||||||
// Throw uncaught exception that will trigger the report system
|
|
||||||
Thread.currentThread().getUncaughtExceptionHandler()
|
|
||||||
.uncaughtException(Thread.currentThread(), throwable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
|
||||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
|
||||||
*/
|
|
||||||
protected void initACRA() {
|
|
||||||
if (ACRA.isACRASenderServiceProcess()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
|
||||||
.withBuildConfigClass(BuildConfig.class);
|
|
||||||
ACRA.init(this, acraConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initNotificationChannels() {
|
|
||||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
|
||||||
// the main and update channels
|
|
||||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.notification_channel_name))
|
|
||||||
.setDescription(getString(R.string.notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.app_update_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.app_update_notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
|
||||||
.setName(getString(R.string.hash_channel_name))
|
|
||||||
.setDescription(getString(R.string.hash_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.error_report_channel_name))
|
|
||||||
.setDescription(getString(R.string.error_report_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.streams_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
|
||||||
.setName(getString(R.string.streams_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.streams_notification_channel_description))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
|
||||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFirstRun() {
|
|
||||||
return isFirstRun;
|
|
||||||
}
|
|
||||||
}
|
|
283
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
283
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.request.allowRgb565
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import io.reactivex.rxjava3.exceptions.CompositeException
|
||||||
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
|
||||||
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import org.acra.ACRA.init
|
||||||
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||||
|
import org.schabi.newpipe.settings.NewPipeSettings
|
||||||
|
import org.schabi.newpipe.util.BridgeStateSaverInitializer
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
import org.schabi.newpipe.util.StateSaver
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InterruptedIOException
|
||||||
|
import java.net.SocketException
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* App.kt is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe 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.
|
||||||
|
*
|
||||||
|
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
@HiltAndroidApp
|
||||||
|
open class App :
|
||||||
|
Application(),
|
||||||
|
SingletonImageLoader.Factory {
|
||||||
|
var isFirstRun = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
initACRA()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
|
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
val lastUsedPrefVersion =
|
||||||
|
PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this)
|
||||||
|
.getInt(getString(R.string.last_used_preferences_version), -1)
|
||||||
|
isFirstRun = lastUsedPrefVersion == -1
|
||||||
|
|
||||||
|
// Initialize settings first because other initializations can use its values
|
||||||
|
NewPipeSettings.initSettings(this)
|
||||||
|
|
||||||
|
NewPipe.init(
|
||||||
|
getDownloader(),
|
||||||
|
Localization.getPreferredLocalization(this),
|
||||||
|
Localization.getPreferredContentCountry(this),
|
||||||
|
)
|
||||||
|
Localization.initPrettyTime(Localization.resolvePrettyTime(this))
|
||||||
|
|
||||||
|
BridgeStateSaverInitializer.init(this)
|
||||||
|
StateSaver.init(this)
|
||||||
|
initNotificationChannels()
|
||||||
|
|
||||||
|
ServiceHelper.initServices(this)
|
||||||
|
|
||||||
|
// Initialize image loader
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
ImageStrategy.setPreferredImageQuality(
|
||||||
|
PreferredImageQuality.fromPreferenceKey(
|
||||||
|
this,
|
||||||
|
prefs.getString(
|
||||||
|
getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
configureRxJavaErrorHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(context: Context): ImageLoader =
|
||||||
|
ImageLoader
|
||||||
|
.Builder(this)
|
||||||
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
|
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
.crossfade(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
protected open fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(null)
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val key = getString(R.string.recaptcha_cookies_key)
|
||||||
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||||
|
downloader.updateYoutubeRestrictedModeCookies(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureRxJavaErrorHandler() {
|
||||||
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
|
RxJavaPlugins.setErrorHandler(
|
||||||
|
object : Consumer<Throwable> {
|
||||||
|
override fun accept(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
|
||||||
|
|
||||||
|
// As UndeliverableException is a wrapper,
|
||||||
|
// get the cause of it to get the "real" exception
|
||||||
|
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
|
||||||
|
|
||||||
|
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
|
||||||
|
|
||||||
|
for (error in errors) {
|
||||||
|
if (isThrowableIgnored(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(actualThrowable)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return throwable // network api cancellation
|
||||||
|
.hasAssignableCause(
|
||||||
|
IOException::class.java,
|
||||||
|
SocketException::class.java, // blocking code disposed
|
||||||
|
InterruptedException::class.java,
|
||||||
|
InterruptedIOException::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return throwable
|
||||||
|
.hasAssignableCause(
|
||||||
|
// bug in app
|
||||||
|
NullPointerException::class.java,
|
||||||
|
IllegalArgumentException::class.java,
|
||||||
|
OnErrorNotImplementedException::class.java,
|
||||||
|
MissingBackpressureException::class.java,
|
||||||
|
// bug in operator
|
||||||
|
IllegalStateException::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportException(throwable: Throwable) {
|
||||||
|
// Throw uncaught exception that will trigger the report system
|
||||||
|
Thread
|
||||||
|
.currentThread()
|
||||||
|
.uncaughtExceptionHandler
|
||||||
|
.uncaughtException(Thread.currentThread(), throwable)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in [.attachBaseContext] after calling the `super` method.
|
||||||
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
|
*/
|
||||||
|
protected fun initACRA() {
|
||||||
|
if (isACRASenderServiceProcess()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val acraConfig =
|
||||||
|
CoreConfigurationBuilder()
|
||||||
|
.withBuildConfigClass(BuildConfig::class.java)
|
||||||
|
init(this, acraConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationChannels() {
|
||||||
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
|
// the main and update channels
|
||||||
|
val mainChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val appUpdateChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.app_update_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.app_update_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val hashChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.hash_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH,
|
||||||
|
).setName(getString(R.string.hash_channel_name))
|
||||||
|
.setDescription(getString(R.string.hash_channel_description))
|
||||||
|
.build()
|
||||||
|
val errorReportChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.error_report_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.error_report_channel_name))
|
||||||
|
.setDescription(getString(R.string.error_report_channel_description))
|
||||||
|
.build()
|
||||||
|
val newStreamChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.streams_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||||
|
).setName(getString(R.string.streams_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isDisposedRxExceptionsReported(): Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||||
|
private val TAG = App::class.java.toString()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var instance: App
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class AppModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
}
|
||||||
|
}
|
@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import com.evernote.android.state.State;
|
||||||
import icepick.State;
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment {
|
public abstract class BaseFragment extends Fragment {
|
||||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
onRestoreInstanceState(savedInstanceState);
|
onRestoreInstanceState(savedInstanceState);
|
||||||
}
|
}
|
||||||
@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
|
@ -44,7 +44,6 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@ -52,7 +51,6 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentContainerView;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@ -66,13 +64,11 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
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;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
@ -170,7 +166,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||||
&& !App.getApp().isFirstRun()
|
&& !App.getInstance().isFirstRun()
|
||||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||||
}
|
}
|
||||||
@ -180,7 +176,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
||||||
final App app = App.getApp();
|
final App app = App.getInstance();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||||
@ -557,39 +553,27 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
final var fragmentManager = getSupportFragmentManager();
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
|
||||||
// delegate the back press to it
|
|
||||||
if (fragment instanceof BackPressable) {
|
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (fragment instanceof CommentRepliesFragment) {
|
|
||||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// to show the top level comments again
|
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
||||||
.findFragmentById(R.id.fragment_player_holder);
|
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragmentPlayer instanceof BackPressable) {
|
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
||||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
return;
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
}
|
||||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
} else {
|
||||||
}
|
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
||||||
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
|
// delegate the back press to it
|
||||||
|
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
||||||
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||||
|
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
if (fragmentManager.getBackStackEntryCount() == 1) {
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
@ -648,15 +632,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
final var fm = getSupportFragmentManager();
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
|
||||||
|
|
||||||
if (fragment instanceof CommentRepliesFragment) {
|
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, true);
|
|
||||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
|
||||||
// If search fragment wasn't found in the backstack go to the main fragment
|
// If search fragment wasn't found in the backstack go to the main fragment
|
||||||
NavigationHelper.gotoMainFragment(fm);
|
NavigationHelper.gotoMainFragment(fm);
|
||||||
}
|
}
|
||||||
@ -854,68 +832,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetailFragmentFromCommentReplies(
|
|
||||||
@NonNull final FragmentManager fm,
|
|
||||||
final boolean popBackStack
|
|
||||||
) {
|
|
||||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
|
||||||
@Nullable final String fragmentUnderEntryName;
|
|
||||||
if (fm.getBackStackEntryCount() < 2) {
|
|
||||||
fragmentUnderEntryName = null;
|
|
||||||
} else {
|
|
||||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
|
||||||
.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// the root comment is the comment for which the user opened the replies page
|
|
||||||
@Nullable final CommentRepliesFragment repliesFragment =
|
|
||||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
|
||||||
@Nullable final CommentsInfoItem rootComment =
|
|
||||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
|
||||||
|
|
||||||
// sometimes this function pops the backstack, other times it's handled by the system
|
|
||||||
if (popBackStack) {
|
|
||||||
fm.popBackStackImmediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
|
||||||
// stacked under the one that is currently being popped
|
|
||||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
|
||||||
.from(mainBinding.fragmentPlayerHolder);
|
|
||||||
// do not return to the comment if the details fragment was closed
|
|
||||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
|
||||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
@Override
|
|
||||||
public void onStateChanged(@NonNull final View bottomSheet,
|
|
||||||
final int newState) {
|
|
||||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
final Fragment detailFragment = fm.findFragmentById(
|
|
||||||
R.id.fragment_player_holder);
|
|
||||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
|
||||||
// should always be the case
|
|
||||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
|
||||||
}
|
|
||||||
behavior.removeBottomSheetCallback(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
|
||||||
// not needed, listener is removed once the sheet is expanded
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
|
|||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||||
@ -98,8 +101,6 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
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;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@ -152,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
getWindow().setAttributes(params);
|
getWindow().setAttributes(params);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
||||||
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
||||||
@ -197,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -138,8 +138,12 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
||||||
),
|
),
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"Icepick", "2015", "Frankie Sardo",
|
"Android-State", "2018", "Evernote",
|
||||||
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
|
"https://github.com/Evernote/android-state", StandardLicenses.EPL1
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Bridge", "2021", "Livefront",
|
||||||
|
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
|
||||||
),
|
),
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||||
|
@ -39,6 +39,8 @@ import androidx.documentfile.provider.DocumentFile;
|
|||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
import com.nononsenseapps.filepicker.Utils;
|
import com.nononsenseapps.filepicker.Utils;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
@ -59,6 +61,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
|||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
|
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||||
|
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
import org.schabi.newpipe.util.FilenameUtils;
|
import org.schabi.newpipe.util.FilenameUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
@ -67,8 +71,6 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
|||||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
|
||||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -79,8 +81,6 @@ import java.util.Locale;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
@ -214,7 +214,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
context = getContext();
|
context = getContext();
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||||
@ -372,7 +372,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.error;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -13,7 +12,6 @@ import android.view.Menu;
|
|||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@ -22,7 +20,6 @@ import androidx.core.content.IntentCompat;
|
|||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
@ -187,25 +184,6 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the checked activity.
|
|
||||||
*
|
|
||||||
* @param returnActivity the activity to return to
|
|
||||||
* @return the casted return activity or null
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
|
||||||
Class<? extends Activity> checkedReturnActivity = null;
|
|
||||||
if (returnActivity != null) {
|
|
||||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
|
||||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
|
||||||
} else {
|
|
||||||
checkedReturnActivity = MainActivity.class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return checkedReturnActivity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildInfo(final ErrorInfo info) {
|
private void buildInfo(final ErrorInfo info) {
|
||||||
String text = "";
|
String text = "";
|
||||||
|
|
||||||
|
@ -185,10 +185,8 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||||||
final int abuseEnd = url.indexOf("+path");
|
final int abuseEnd = url.indexOf("+path");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
|
||||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
} catch (final StringIndexOutOfBoundsException e) {
|
||||||
handleCookies(abuseCookie);
|
|
||||||
} catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
|
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
||||||
|
@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
@ -22,8 +24,6 @@ import org.schabi.newpipe.util.InfoCache;
|
|||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||||
@State
|
@State
|
||||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||||
@ -134,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
hideErrorPanel();
|
hideErrorPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void showEmptyState() {
|
public void showEmptyState() {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
if (emptyStateView != null) {
|
if (emptyStateView != null) {
|
||||||
|
@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
@ -73,7 +74,6 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
|
|||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.Image;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
@ -127,8 +127,7 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import coil.util.CoilUtils;
|
import coil3.util.CoilUtils;
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@ -881,8 +880,7 @@ public final class VideoDetailFragment
|
|||||||
tabContentDescriptions.clear();
|
tabContentDescriptions.clear();
|
||||||
|
|
||||||
if (shouldShowComments()) {
|
if (shouldShowComments()) {
|
||||||
pageAdapter.addFragment(
|
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
||||||
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
|
|
||||||
tabIcons.add(R.drawable.ic_comment);
|
tabIcons.add(R.drawable.ic_comment);
|
||||||
tabContentDescriptions.add(R.string.comments_tab_description);
|
tabContentDescriptions.add(R.string.comments_tab_description);
|
||||||
}
|
}
|
||||||
@ -1012,20 +1010,6 @@ public final class VideoDetailFragment
|
|||||||
updateTabLayoutVisibility();
|
updateTabLayoutVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scrollToComment(final CommentsInfoItem comment) {
|
|
||||||
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
|
||||||
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
|
||||||
if (!(fragment instanceof CommentsFragment)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unexpand the app bar only if scrolling to the comment succeeded
|
|
||||||
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
|
||||||
binding.appBarLayout.setExpanded(false, false);
|
|
||||||
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play Utils
|
// Play Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -9,6 +9,8 @@ import android.view.View;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
@ -24,7 +26,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@ -143,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||||||
currentWorker = loadResult(forceLoad)
|
currentWorker = loadResult(forceLoad)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@NonNull L result) -> {
|
.subscribe((@NonNull final L result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
currentNextPage = result.getNextPage();
|
currentNextPage = result.getNextPage();
|
||||||
|
@ -25,6 +25,7 @@ import androidx.core.graphics.ColorUtils;
|
|||||||
import androidx.core.view.MenuProvider;
|
import androidx.core.view.MenuProvider;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.jakewharton.rxbinding4.view.RxView;
|
import com.jakewharton.rxbinding4.view.RxView;
|
||||||
@ -59,8 +60,7 @@ import java.util.List;
|
|||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import coil.util.CoilUtils;
|
import coil3.util.CoilUtils;
|
||||||
import icepick.State;
|
|
||||||
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;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@ -249,7 +249,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void monitorSubscription(final ChannelInfo info) {
|
private void monitorSubscription(final ChannelInfo info) {
|
||||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
final Consumer<Throwable> onError = (final Throwable throwable) -> {
|
||||||
animate(binding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||||
"Get subscription status", currentInfo));
|
"Get subscription status", currentInfo));
|
||||||
@ -284,14 +284,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull final Object o) -> {
|
||||||
subscriptionManager.insertSubscription(subscription);
|
subscriptionManager.insertSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull final Object o) -> {
|
||||||
subscriptionManager.deleteSubscription(subscription);
|
subscriptionManager.deleteSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
@ -318,7 +318,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
final Consumer<Object> onNext = (@NonNull final Object o) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Changed subscription status to this channel!");
|
Log.d(TAG, "Changed subscription status to this channel!");
|
||||||
}
|
}
|
||||||
@ -338,7 +338,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
return (final List<SubscriptionEntity> subscriptionEntities) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||||
|
@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
@ -32,13 +34,12 @@ import java.util.List;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
// states must be protected and not private for IcePick being able to access them
|
// states must be protected and not private for State being able to access them
|
||||||
@State
|
@State
|
||||||
protected ListLinkHandler tabHandler;
|
protected ListLinkHandler tabHandler;
|
||||||
@State
|
@State
|
||||||
@ -156,6 +157,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||||
.filter(StreamInfoItem.class::isInstance)
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
@ -1,170 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.image.CoilHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public final class CommentRepliesFragment
|
|
||||||
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
|
||||||
|
|
||||||
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
@State
|
|
||||||
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Constructors and lifecycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
// only called by the Android framework, after which readFrom is called and restores all data
|
|
||||||
public CommentRepliesFragment() {
|
|
||||||
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
|
||||||
this();
|
|
||||||
this.commentsInfoItem = commentsInfoItem;
|
|
||||||
// setting "" as title since the title will be properly set right after
|
|
||||||
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
disposables.clear();
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
return () -> {
|
|
||||||
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
final CommentsInfoItem item = commentsInfoItem;
|
|
||||||
|
|
||||||
// load the author avatar
|
|
||||||
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
|
|
||||||
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
|
||||||
? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// setup author name and comment date
|
|
||||||
binding.authorName.setText(item.getUploaderName());
|
|
||||||
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
|
||||||
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
|
||||||
binding.authorTouchArea.setOnClickListener(
|
|
||||||
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
|
||||||
|
|
||||||
// setup like count, hearted and pinned
|
|
||||||
binding.thumbsUpCount.setText(
|
|
||||||
Localization.likeCount(requireContext(), item.getLikeCount()));
|
|
||||||
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
|
||||||
// not to use a different margin only when both the next two views are gone
|
|
||||||
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
|
||||||
.setMarginEnd(DeviceUtils.dpToPx(
|
|
||||||
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
|
||||||
requireContext()));
|
|
||||||
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// setup comment content
|
|
||||||
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
|
||||||
item.getUrl(), disposables, null);
|
|
||||||
|
|
||||||
return binding.getRoot();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// State saving
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeTo(final Queue<Object> objectsToSave) {
|
|
||||||
super.writeTo(objectsToSave);
|
|
||||||
objectsToSave.add(commentsInfoItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
|
||||||
super.readFrom(savedObjects);
|
|
||||||
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Data loading
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
|
||||||
// the reply count string will be shown as the activity title
|
|
||||||
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
|
||||||
// commentsInfoItem.getUrl() should contain the url of the original
|
|
||||||
// ListInfo<CommentsInfoItem>, which should be the stream url
|
|
||||||
return ExtractorHelper.getMoreCommentItems(
|
|
||||||
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
return ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the comment to which the replies are shown
|
|
||||||
*/
|
|
||||||
public CommentsInfoItem getCommentsInfoItem() {
|
|
||||||
return commentsInfoItem;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
|
||||||
/**
|
|
||||||
* This class is used to wrap the comment replies page into a ListInfo object.
|
|
||||||
*
|
|
||||||
* @param comment the comment from which to get replies
|
|
||||||
* @param name will be shown as the fragment title
|
|
||||||
*/
|
|
||||||
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
|
||||||
super(comment.getServiceId(),
|
|
||||||
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
|
||||||
setNextPage(comment.getReplies());
|
|
||||||
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
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.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
private TextView emptyStateDesc;
|
|
||||||
|
|
||||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
|
||||||
final String name) {
|
|
||||||
final CommentsFragment instance = new CommentsFragment();
|
|
||||||
instance.setInitialData(serviceId, url, name);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentsFragment() {
|
|
||||||
super(UserAction.REQUESTED_COMMENTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
|
||||||
super.initViews(rootView, savedInstanceState);
|
|
||||||
|
|
||||||
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Load and handle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
|
||||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Contract
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleResult(@NonNull final CommentsInfo result) {
|
|
||||||
super.handleResult(result);
|
|
||||||
|
|
||||||
emptyStateDesc.setText(
|
|
||||||
result.isCommentsDisabled()
|
|
||||||
? R.string.comments_are_disabled
|
|
||||||
: R.string.no_comments);
|
|
||||||
|
|
||||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
return ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean scrollToComment(final CommentsInfoItem comment) {
|
|
||||||
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
|
||||||
if (position < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsList.scrollToPosition(position);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,35 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.comments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.compose.content
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentSection
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.util.KEY_URL
|
||||||
|
|
||||||
|
class CommentsFragment : Fragment() {
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = content {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply {
|
||||||
|
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
@ -29,7 +31,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import coil.util.CoilUtils;
|
import coil3.util.CoilUtils;
|
||||||
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;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
@ -40,6 +40,8 @@ import androidx.preference.PreferenceManager;
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
@ -77,7 +79,6 @@ import java.util.Queue;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
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;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@ -550,7 +551,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onFocusChange() called with: "
|
Log.d(TAG, "onFocusChange() called with: "
|
||||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||||
@ -611,7 +612,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
};
|
};
|
||||||
searchEditText.addTextChangedListener(textWatcher);
|
searchEditText.addTextChangedListener(textWatcher);
|
||||||
searchEditText.setOnEditorActionListener(
|
searchEditText.setOnEditorActionListener(
|
||||||
(TextView v, int actionId, KeyEvent event) -> {
|
(final TextView v, final int actionId, final KeyEvent event) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
||||||
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
||||||
|
@ -2,14 +2,12 @@ package org.schabi.newpipe.fragments.list.videos
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.compose.content
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
import org.schabi.newpipe.ktx.serializable
|
import org.schabi.newpipe.ktx.serializable
|
||||||
import org.schabi.newpipe.ui.components.video.RelatedItems
|
import org.schabi.newpipe.ui.components.video.RelatedItems
|
||||||
@ -21,15 +19,10 @@ class RelatedItemsFragment : Fragment() {
|
|||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
) = content {
|
||||||
return ComposeView(requireContext()).apply {
|
AppTheme {
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
setContent {
|
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
||||||
AppTheme {
|
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
|
||||||
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ 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.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
@ -75,21 +74,16 @@ public class InfoItemBuilder {
|
|||||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||||
@NonNull final InfoItem.InfoType infoType,
|
@NonNull final InfoItem.InfoType infoType,
|
||||||
final boolean useMiniVariant) {
|
final boolean useMiniVariant) {
|
||||||
switch (infoType) {
|
return switch (infoType) {
|
||||||
case STREAM:
|
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
: new StreamInfoItemHolder(this, parent);
|
||||||
: new StreamInfoItemHolder(this, parent);
|
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||||
case CHANNEL:
|
: new ChannelInfoItemHolder(this, parent);
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
: new ChannelInfoItemHolder(this, parent);
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case PLAYLIST:
|
case COMMENT ->
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
throw new IllegalArgumentException("Comments should be rendered using Compose");
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
};
|
||||||
case COMMENT:
|
|
||||||
return new CommentInfoItemHolder(this, parent);
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
public Context getContext() {
|
||||||
|
@ -21,7 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
|||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
@ -283,46 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
Log.d(TAG, "onCreateViewHolder() called with: "
|
Log.d(TAG, "onCreateViewHolder() called with: "
|
||||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||||
}
|
}
|
||||||
switch (type) {
|
return switch (type) {
|
||||||
// #4475 and #3368
|
// #4475 and #3368
|
||||||
// Always create a new instance otherwise the same instance
|
// Always create a new instance otherwise the same instance
|
||||||
// is sometimes reused which causes a crash
|
// is sometimes reused which causes a crash
|
||||||
case HEADER_TYPE:
|
case HEADER_TYPE -> new HFHolder(headerSupplier.get());
|
||||||
return new HFHolder(headerSupplier.get());
|
case FOOTER_TYPE -> new HFHolder(PignateFooterBinding
|
||||||
case FOOTER_TYPE:
|
.inflate(layoutInflater, parent, false)
|
||||||
return new HFHolder(PignateFooterBinding
|
.getRoot()
|
||||||
.inflate(layoutInflater, parent, false)
|
);
|
||||||
.getRoot()
|
case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
);
|
case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_STREAM_HOLDER_TYPE:
|
case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
case STREAM_HOLDER_TYPE:
|
case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||||
case GRID_STREAM_HOLDER_TYPE:
|
case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CARD_STREAM_HOLDER_TYPE:
|
case MINI_PLAYLIST_HOLDER_TYPE ->
|
||||||
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_CHANNEL_HOLDER_TYPE:
|
case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
case GRID_PLAYLIST_HOLDER_TYPE ->
|
||||||
case CHANNEL_HOLDER_TYPE:
|
new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
case CARD_PLAYLIST_HOLDER_TYPE ->
|
||||||
case CARD_CHANNEL_HOLDER_TYPE:
|
new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
default -> new FallbackViewHolder(new View(parent.getContext()));
|
||||||
case GRID_CHANNEL_HOLDER_TYPE:
|
};
|
||||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case COMMENT_HOLDER_TYPE:
|
|
||||||
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
default:
|
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -346,7 +346,7 @@ public final class InfoItemDialog {
|
|||||||
|
|
||||||
public static void reportErrorDuringInitialization(final Throwable throwable,
|
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||||
final InfoItem item) {
|
final InfoItem item) {
|
||||||
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
|
||||||
throwable,
|
throwable,
|
||||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||||
"none",
|
"none",
|
||||||
|
@ -1,208 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
|
|
||||||
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.ClickableSpan;
|
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.image.CoilHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
|
||||||
|
|
||||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
|
||||||
private final int commentHorizontalPadding;
|
|
||||||
private final int commentVerticalPadding;
|
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
|
||||||
private final ImageView itemThumbnailView;
|
|
||||||
private final TextView itemContentView;
|
|
||||||
private final ImageView itemThumbsUpView;
|
|
||||||
private final TextView itemLikesCountView;
|
|
||||||
private final TextView itemTitleView;
|
|
||||||
private final ImageView itemHeartView;
|
|
||||||
private final ImageView itemPinnedView;
|
|
||||||
private final Button repliesButton;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final TextEllipsizer textEllipsizer;
|
|
||||||
|
|
||||||
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
|
||||||
|
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
|
||||||
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
|
||||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
|
||||||
repliesButton = itemView.findViewById(R.id.replies_button);
|
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
|
||||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
|
||||||
|
|
||||||
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
|
||||||
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
|
||||||
if (Boolean.TRUE.equals(isEllipsized)) {
|
|
||||||
denyLinkFocus();
|
|
||||||
} else {
|
|
||||||
determineMovementMethod();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem item)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the author avatar
|
|
||||||
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
|
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
|
||||||
} else {
|
|
||||||
itemThumbnailView.setVisibility(View.GONE);
|
|
||||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
|
||||||
commentHorizontalPadding, commentVerticalPadding);
|
|
||||||
}
|
|
||||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
|
||||||
|
|
||||||
|
|
||||||
// setup the top row, with pinned icon, author name and comment date
|
|
||||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
|
|
||||||
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
|
|
||||||
item.getTextualUploadDate())));
|
|
||||||
|
|
||||||
|
|
||||||
// setup bottom row, with likes, heart and replies button
|
|
||||||
itemLikesCountView.setText(
|
|
||||||
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
|
||||||
|
|
||||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
final boolean hasReplies = item.getReplies() != null;
|
|
||||||
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
|
||||||
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
|
||||||
repliesButton.setText(hasReplies
|
|
||||||
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
|
||||||
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
|
||||||
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
|
||||||
|
|
||||||
|
|
||||||
// setup comment content and click listeners to expand/ellipsize it
|
|
||||||
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
|
||||||
textEllipsizer.setStreamUrl(item.getUrl());
|
|
||||||
textEllipsizer.setContent(item.getCommentText());
|
|
||||||
textEllipsizer.ellipsize();
|
|
||||||
|
|
||||||
//noinspection ClickableViewAccessibility
|
|
||||||
itemContentView.setOnTouchListener((v, event) -> {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text instanceof Spanned buffer) {
|
|
||||||
final int action = event.getAction();
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
|
||||||
final int offset = getOffsetForHorizontalLine(itemContentView, event);
|
|
||||||
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
|
|
||||||
|
|
||||||
if (links.length != 0) {
|
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
|
||||||
links[0].onClick(itemContentView);
|
|
||||||
}
|
|
||||||
// we handle events that intersect links, so return true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
|
||||||
textEllipsizer.toggle();
|
|
||||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
|
||||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
|
||||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
|
||||||
openCommentAuthor(item);
|
|
||||||
} else {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text != null) {
|
|
||||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
|
||||||
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
|
||||||
item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
|
||||||
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
|
||||||
item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void allowLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void denyLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldFocusLinks() {
|
|
||||||
if (itemView.isInTouchMode()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final URLSpan[] urls = itemContentView.getUrls();
|
|
||||||
|
|
||||||
return urls != null && urls.length != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void determineMovementMethod() {
|
|
||||||
if (shouldFocusLinks()) {
|
|
||||||
allowLinkFocus();
|
|
||||||
} else {
|
|
||||||
denyLinkFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -19,6 +19,8 @@ import androidx.fragment.app.FragmentManager;
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
@ -36,16 +38,15 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
|||||||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
|
||||||
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.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
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 com.evernote.android.state.State
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
import com.xwray.groupie.OnItemClickListener
|
import com.xwray.groupie.OnItemClickListener
|
||||||
import com.xwray.groupie.OnItemLongClickListener
|
import com.xwray.groupie.OnItemLongClickListener
|
||||||
import icepick.State
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
@ -165,7 +165,7 @@ class FeedViewModel(
|
|||||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
FeedViewModel(
|
FeedViewModel(
|
||||||
App.getApp(),
|
App.instance,
|
||||||
groupId,
|
groupId,
|
||||||
// Read initial value from preferences
|
// Read initial value from preferences
|
||||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||||
|
@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.viewbinding.ViewBinding;
|
import androidx.viewbinding.ViewBinding;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
@ -45,7 +46,6 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@ -368,6 +368,7 @@ public class StatisticsPlaylistFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewbinding.ViewBinding;
|
import androidx.viewbinding.ViewBinding;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
@ -49,12 +51,12 @@ 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.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.debounce.DebounceSavable;
|
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
|
||||||
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.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -63,7 +65,6 @@ import java.util.List;
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@ -843,6 +844,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.local.subscription;
|
package org.schabi.newpipe.local.subscription;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@ -10,13 +12,11 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
public class ImportConfirmationDialog extends DialogFragment {
|
public class ImportConfirmationDialog extends DialogFragment {
|
||||||
@State
|
@State
|
||||||
protected Intent resultServiceIntent;
|
protected Intent resultServiceIntent;
|
||||||
@ -57,12 +57,12 @@ public class ImportConfirmationDialog extends DialogFragment {
|
|||||||
throw new IllegalStateException("Result intent is null");
|
throw new IllegalStateException("Result intent is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.evernote.android.state.State
|
||||||
import com.xwray.groupie.Group
|
import com.xwray.groupie.Group
|
||||||
import com.xwray.groupie.GroupAdapter
|
import com.xwray.groupie.GroupAdapter
|
||||||
import com.xwray.groupie.Section
|
import com.xwray.groupie.Section
|
||||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
import icepick.State
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
||||||
|
@ -27,6 +27,8 @@ import androidx.annotation.StringRes;
|
|||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.core.text.util.LinkifyCompat;
|
import androidx.core.text.util.LinkifyCompat;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
@ -44,8 +46,6 @@ import org.schabi.newpipe.util.ServiceHelper;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public class SubscriptionsImportFragment extends BaseFragment {
|
public class SubscriptionsImportFragment extends BaseFragment {
|
||||||
@State
|
@State
|
||||||
int currentServiceId = Constants.NO_SERVICE_ID;
|
int currentServiceId = Constants.NO_SERVICE_ID;
|
||||||
|
@ -18,11 +18,11 @@ import androidx.lifecycle.Observer
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.evernote.android.state.State
|
||||||
|
import com.livefront.bridge.Bridge
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.OnItemClickListener
|
import com.xwray.groupie.OnItemClickListener
|
||||||
import com.xwray.groupie.Section
|
import com.xwray.groupie.Section
|
||||||
import icepick.Icepick
|
|
||||||
import icepick.State
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
||||||
@ -78,7 +78,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||||
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
|
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
|
||||||
@ -115,7 +115,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
|
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
|
||||||
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
|
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
|
||||||
|
|
||||||
Icepick.saveInstanceState(this, outState)
|
Bridge.saveInstanceState(this, outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -11,10 +11,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.evernote.android.state.State
|
||||||
|
import com.livefront.bridge.Bridge
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.TouchCallback
|
import com.xwray.groupie.TouchCallback
|
||||||
import icepick.Icepick
|
|
||||||
import icepick.State
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
||||||
@ -23,10 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
|||||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.collections.List
|
|
||||||
import kotlin.collections.map
|
|
||||||
import kotlin.collections.sortedBy
|
|
||||||
|
|
||||||
class FeedGroupReorderDialog : DialogFragment() {
|
class FeedGroupReorderDialog : DialogFragment() {
|
||||||
private var _binding: DialogFeedGroupReorderBinding? = null
|
private var _binding: DialogFeedGroupReorderBinding? = null
|
||||||
@ -42,7 +38,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||||
}
|
}
|
||||||
@ -80,7 +76,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
Icepick.saveInstanceState(this, outState)
|
Bridge.saveInstanceState(this, outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGroups(list: List<FeedGroupEntity>) {
|
private fun handleGroups(list: List<FeedGroupEntity>) {
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package org.schabi.newpipe.paging
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
|
||||||
|
class CommentRepliesSource(
|
||||||
|
private val commentInfo: CommentsInfoItem,
|
||||||
|
) : PagingSource<Page, CommentsInfoItem>() {
|
||||||
|
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||||
|
// params.key is null the first time load() is called, and we need to return the first page
|
||||||
|
val repliesPage = params.key ?: commentInfo.replies
|
||||||
|
val info = withContext(Dispatchers.IO) {
|
||||||
|
CommentsInfo.getMoreItems(service, commentInfo.url, repliesPage)
|
||||||
|
}
|
||||||
|
return LoadResult.Page(info.items, null, info.nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.schabi.newpipe.paging
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||||
|
|
||||||
|
class CommentsSource(private val commentInfo: CommentInfo) : PagingSource<Page, CommentsInfoItem>() {
|
||||||
|
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||||
|
// params.key is null the first time the load() function is called, so we need to return the
|
||||||
|
// first batch of already-loaded comments
|
||||||
|
if (params.key == null) {
|
||||||
|
return LoadResult.Page(commentInfo.comments, null, commentInfo.nextPage)
|
||||||
|
} else {
|
||||||
|
val info = withContext(Dispatchers.IO) {
|
||||||
|
CommentsInfo.getMoreItems(service, commentInfo.url, params.key)
|
||||||
|
}
|
||||||
|
return LoadResult.Page(info.items, null, info.nextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||||
|
}
|
@ -46,6 +46,7 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
|||||||
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static coil3.Image_androidKt.toBitmap;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -53,14 +54,12 @@ import android.content.Intent;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.graphics.drawable.DrawableKt;
|
|
||||||
import androidx.core.math.MathUtils;
|
import androidx.core.math.MathUtils;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@ -125,7 +124,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import coil.target.Target;
|
import coil3.target.Target;
|
||||||
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;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@ -193,7 +192,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private Bitmap currentThumbnail;
|
private Bitmap currentThumbnail;
|
||||||
@Nullable
|
@Nullable
|
||||||
private coil.request.Disposable thumbnailDisposable;
|
private coil3.request.Disposable thumbnailDisposable;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player
|
// Player
|
||||||
@ -789,27 +788,26 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
// scale down the notification thumbnail for performance
|
// scale down the notification thumbnail for performance
|
||||||
final var thumbnailTarget = new Target() {
|
final var thumbnailTarget = new Target() {
|
||||||
@Override
|
@Override
|
||||||
public void onError(@Nullable final Drawable error) {
|
public void onError(@Nullable final coil3.Image error) {
|
||||||
Log.e(TAG, "Thumbnail - onError() called");
|
Log.e(TAG, "Thumbnail - onError() called");
|
||||||
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||||
onThumbnailLoaded(null);
|
onThumbnailLoaded(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart(@Nullable final Drawable placeholder) {
|
public void onStart(@Nullable final coil3.Image placeholder) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - onStart() called");
|
Log.d(TAG, "Thumbnail - onStart() called");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(@NonNull final Drawable result) {
|
public void onSuccess(@NonNull final coil3.Image result) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]");
|
Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]");
|
||||||
}
|
}
|
||||||
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||||
onThumbnailLoaded(DrawableKt.toBitmapOrNull(result, result.getIntrinsicWidth(),
|
onThumbnailLoaded(toBitmap(result));
|
||||||
result.getIntrinsicHeight(), null));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
thumbnailDisposable = CoilHelper.INSTANCE
|
thumbnailDisposable = CoilHelper.INSTANCE
|
||||||
|
@ -24,6 +24,9 @@ import androidx.core.math.MathUtils;
|
|||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
@ -37,9 +40,6 @@ import java.util.function.DoubleConsumer;
|
|||||||
import java.util.function.DoubleFunction;
|
import java.util.function.DoubleFunction;
|
||||||
import java.util.function.DoubleSupplier;
|
import java.util.function.DoubleSupplier;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public class PlaybackParameterDialog extends DialogFragment {
|
public class PlaybackParameterDialog extends DialogFragment {
|
||||||
private static final String TAG = "PlaybackParameterDialog";
|
private static final String TAG = "PlaybackParameterDialog";
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -146,7 +146,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||||
assureCorrectAppLanguage(getContext());
|
assureCorrectAppLanguage(getContext());
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
|
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
|
||||||
initUI();
|
initUI();
|
||||||
|
@ -16,8 +16,8 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.PlayerService;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||||
@ -116,7 +116,7 @@ public final class PlayerHolder {
|
|||||||
// helper to handle context in common place as using the same
|
// helper to handle context in common place as using the same
|
||||||
// context to bind/unbind a service is crucial
|
// context to bind/unbind a service is crucial
|
||||||
private Context getCommonContext() {
|
private Context getCommonContext() {
|
||||||
return App.getApp();
|
return App.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startService(final boolean playAfterConnect,
|
public void startService(final boolean playAfterConnect,
|
||||||
|
@ -179,7 +179,7 @@ public class SeekbarPreviewThumbnailHolder {
|
|||||||
|
|
||||||
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
||||||
// Ensure that you are not running on the main thread, otherwise this will hang
|
// Ensure that you are not running on the main thread, otherwise this will hang
|
||||||
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getApp(), url);
|
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url);
|
||||||
|
|
||||||
if (sw != null) {
|
if (sw != null) {
|
||||||
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "
|
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "
|
||||||
|
@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.localization.Localization;
|
|||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import coil.Coil;
|
import coil3.SingletonImageLoader;
|
||||||
|
|
||||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
private String youtubeRestrictedModeEnabledKey;
|
private String youtubeRestrictedModeEnabledKey;
|
||||||
@ -41,7 +41,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||||||
(preference, newValue) -> {
|
(preference, newValue) -> {
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
||||||
.fromPreferenceKey(requireContext(), (String) newValue));
|
.fromPreferenceKey(requireContext(), (String) newValue));
|
||||||
final var loader = Coil.imageLoader(preference.getContext());
|
final var loader = SingletonImageLoader.get(preference.getContext());
|
||||||
loader.getMemoryCache().clear();
|
loader.getMemoryCache().clear();
|
||||||
loader.getDiskCache().clear();
|
loader.getDiskCache().clear();
|
||||||
Toast.makeText(preference.getContext(),
|
Toast.makeText(preference.getContext(),
|
||||||
|
27
app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt
Normal file
27
app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
|
||||||
|
import org.schabi.newpipe.ui.SwitchPreference
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
|
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
SwitchPreference(
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
R.string.settings_layout_redesign,
|
||||||
|
settingsLayoutRedesign,
|
||||||
|
viewModel::toggleSettingsLayoutRedesign
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.decodeUrlUtf8;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -30,7 +29,6 @@ import org.schabi.newpipe.util.FilePickerActivityHelper;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
|
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
|
||||||
@ -107,28 +105,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||||||
|
|
||||||
private void showPathInSummary(final String prefKey, @StringRes final int defaultString,
|
private void showPathInSummary(final String prefKey, @StringRes final int defaultString,
|
||||||
final Preference target) {
|
final Preference target) {
|
||||||
String rawUri = defaultPreferences.getString(prefKey, null);
|
final Uri uri = Uri.parse(defaultPreferences.getString(prefKey, ""));
|
||||||
if (rawUri == null || rawUri.isEmpty()) {
|
if (uri.equals(Uri.EMPTY)) {
|
||||||
target.setSummary(getString(defaultString));
|
target.setSummary(getString(defaultString));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawUri.charAt(0) == File.separatorChar) {
|
final String summary = ContentResolver.SCHEME_FILE.equals(uri.getScheme())
|
||||||
target.setSummary(rawUri);
|
? uri.getPath() : uri.toString();
|
||||||
return;
|
target.setSummary(summary);
|
||||||
}
|
|
||||||
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
|
|
||||||
target.setSummary(new File(URI.create(rawUri)).getPath());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
rawUri = decodeUrlUtf8(rawUri);
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
target.setSummary(rawUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFileUri(final String path) {
|
private boolean isFileUri(final String path) {
|
||||||
|
@ -156,7 +156,7 @@ public final class NewPipeSettings {
|
|||||||
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
||||||
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
||||||
|
|
||||||
if (App.getApp().isFirstRun()
|
if (App.getInstance().isFirstRun()
|
||||||
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
||||||
setMediaTunneling(context);
|
setMediaTunneling(context);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -18,8 +20,6 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In order to add a migration, follow these steps, given P is the previous version:<br>
|
* In order to add a migration, follow these steps, given P is the previous version:<br>
|
||||||
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
|
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
|
||||||
@ -171,7 +171,7 @@ public final class SettingMigrations {
|
|||||||
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
||||||
|
|
||||||
// no migration to run, already up to date
|
// no migration to run, already up to date
|
||||||
if (App.getApp().isFirstRun()) {
|
if (App.getInstance().isFirstRun()) {
|
||||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||||
return;
|
return;
|
||||||
} else if (lastPrefVersion == VERSION) {
|
} else if (lastPrefVersion == VERSION) {
|
||||||
|
@ -21,7 +21,9 @@ import androidx.fragment.app.FragmentManager;
|
|||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
import com.jakewharton.rxbinding4.widget.RxTextView;
|
import com.jakewharton.rxbinding4.widget.RxTextView;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
@ -41,9 +43,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
|||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 31.08.15.
|
* Created by Christian Schabesberger on 31.08.15.
|
||||||
*
|
*
|
||||||
@ -93,7 +92,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
|||||||
assureCorrectAppLanguage(this);
|
assureCorrectAppLanguage(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceBundle);
|
super.onCreate(savedInstanceBundle);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceBundle);
|
Bridge.restoreInstanceState(this, savedInstanceBundle);
|
||||||
final boolean restored = savedInstanceBundle != null;
|
final boolean restored = savedInstanceBundle != null;
|
||||||
|
|
||||||
final SettingsLayoutBinding settingsLayoutBinding =
|
final SettingsLayoutBinding settingsLayoutBinding =
|
||||||
@ -125,7 +124,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
|||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.TextPreference
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
onSelectSettingOption: (SettingsScreenKey) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
TextPreference(
|
||||||
|
title = R.string.settings_category_debug_title,
|
||||||
|
onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) }
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = Color.Black)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
|
||||||
|
import org.schabi.newpipe.ui.Toolbar
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
|
const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY"
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SettingsV2Activity : ComponentActivity() {
|
||||||
|
|
||||||
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) }
|
||||||
|
navController.addOnDestinationChangedListener { _, _, arguments ->
|
||||||
|
screenTitle =
|
||||||
|
arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
Scaffold(topBar = {
|
||||||
|
Toolbar(
|
||||||
|
title = stringResource(id = screenTitle),
|
||||||
|
hasSearch = true,
|
||||||
|
onSearchQueryChange = null // TODO: Add suggestions logic
|
||||||
|
)
|
||||||
|
}) { padding ->
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = SettingsScreenKey.ROOT.name,
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
SettingsScreenKey.ROOT.name,
|
||||||
|
listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle))
|
||||||
|
) {
|
||||||
|
SettingsScreen(onSelectSettingOption = { screen ->
|
||||||
|
navController.navigate(screen.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
SettingsScreenKey.DEBUG.name,
|
||||||
|
listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle))
|
||||||
|
) {
|
||||||
|
DebugScreen(settingsViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createScreenTitleArg(@StringRes screenTitle: Int) = navArgument(SCREEN_TITLE_KEY) {
|
||||||
|
defaultValue = screenTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SettingsScreenKey(@StringRes val screenTitle: Int) {
|
||||||
|
ROOT(R.string.settings),
|
||||||
|
DEBUG(R.string.settings_category_debug_title)
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.schabi.newpipe.settings.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsViewModel @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
private val preferenceManager: SharedPreferences
|
||||||
|
) : AndroidViewModel(context.applicationContext as Application) {
|
||||||
|
|
||||||
|
private var _settingsLayoutRedesignPref: Boolean
|
||||||
|
get() = preferenceManager.getBoolean(
|
||||||
|
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key), false
|
||||||
|
)
|
||||||
|
set(value) {
|
||||||
|
preferenceManager.edit().putBoolean(
|
||||||
|
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key),
|
||||||
|
value
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
private val _settingsLayoutRedesign: MutableStateFlow<Boolean> =
|
||||||
|
MutableStateFlow(_settingsLayoutRedesignPref)
|
||||||
|
val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow()
|
||||||
|
|
||||||
|
fun toggleSettingsLayoutRedesign(newState: Boolean) {
|
||||||
|
_settingsLayoutRedesign.value = newState
|
||||||
|
_settingsLayoutRedesignPref = newState
|
||||||
|
}
|
||||||
|
}
|
53
app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt
Normal file
53
app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchPreference(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
@StringRes title: Int,
|
||||||
|
isChecked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
@StringRes summary: Int? = null
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
summary?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = summary),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
|
||||||
|
Switch(checked = isChecked, onCheckedChange = onCheckedChange)
|
||||||
|
}
|
||||||
|
}
|
66
app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt
Normal file
66
app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextPreference(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
@StringRes title: Int,
|
||||||
|
@DrawableRes icon: Int? = null,
|
||||||
|
@StringRes summary: Int? = null,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SizeTokens.SpacingSmall)
|
||||||
|
.defaultMinSize(minHeight = SizeTokens.SpaceMinSize)
|
||||||
|
.clickable { onClick() }
|
||||||
|
) {
|
||||||
|
icon?.let {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = "icon for $title preference"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
summary?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = summary),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DescriptionText(
|
||||||
|
description: Description,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
style: TextStyle = LocalTextStyle.current
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = rememberParsedDescription(description),
|
||||||
|
maxLines = maxLines,
|
||||||
|
style = style,
|
||||||
|
overflow = overflow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberParsedDescription(description: Description): AnnotatedString {
|
||||||
|
// TODO: Handle links and hashtags, Markdown.
|
||||||
|
return remember(description) {
|
||||||
|
if (description.type == Description.HTML) {
|
||||||
|
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||||
|
AnnotatedString.fromHtml(description.content, styles)
|
||||||
|
} else {
|
||||||
|
AnnotatedString(description.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingIndicator(modifier: Modifier = Modifier) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = modifier.fillMaxSize().wrapContentSize(Alignment.Center),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import my.nanihadesuka.compose.ScrollbarSettings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun defaultThemedScrollbarSettings(): ScrollbarSettings = ScrollbarSettings.Default.copy(
|
||||||
|
thumbUnselectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyColumnThemedScrollbar(
|
||||||
|
state: LazyListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
settings: ScrollbarSettings = defaultThemedScrollbarSettings(),
|
||||||
|
indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
my.nanihadesuka.compose.LazyColumnScrollbar(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
settings = settings,
|
||||||
|
indicatorContent = indicatorContent,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
@ -16,13 +16,13 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.window.core.layout.WindowWidthSizeClass
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
import my.nanihadesuka.compose.LazyColumnScrollbar
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.InfoItem
|
import org.schabi.newpipe.extractor.InfoItem
|
||||||
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.info_list.ItemViewMode
|
import org.schabi.newpipe.info_list.ItemViewMode
|
||||||
import org.schabi.newpipe.ktx.findFragmentActivity
|
import org.schabi.newpipe.ktx.findFragmentActivity
|
||||||
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
|
import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
|
||||||
import org.schabi.newpipe.ui.components.items.stream.StreamListItem
|
import org.schabi.newpipe.ui.components.items.stream.StreamListItem
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper
|
import org.schabi.newpipe.util.DependentPreferenceHelper
|
||||||
@ -72,7 +72,7 @@ fun ItemList(
|
|||||||
} else {
|
} else {
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
|
|
||||||
LazyColumnScrollbar(state = state) {
|
LazyColumnThemedScrollbar(state = state) {
|
||||||
LazyColumn(modifier = nestedScrollModifier, state = state) {
|
LazyColumn(modifier = nestedScrollModifier, state = state) {
|
||||||
listHeader()
|
listHeader()
|
||||||
|
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
package org.schabi.newpipe.ui.components.items.playlist
|
package org.schabi.newpipe.ui.components.items.playlist
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
@ -46,10 +47,10 @@ fun PlaylistThumbnail(
|
|||||||
.padding(2.dp),
|
.padding(2.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_playlist_play),
|
imageVector = Icons.AutoMirrored.Default.PlaylistPlay,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(Color.White),
|
tint = Color.White,
|
||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -96,8 +96,10 @@ fun StreamMenu(
|
|||||||
val list = listOf(StreamEntity(stream))
|
val list = listOf(StreamEntity(stream))
|
||||||
PlaylistDialog.createCorrespondingDialog(context, list) { dialog ->
|
PlaylistDialog.createCorrespondingDialog(context, list) { dialog ->
|
||||||
val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
|
val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
|
||||||
val fragmentManager = context.findFragmentActivity().supportFragmentManager
|
dialog.show(
|
||||||
dialog.show(fragmentManager, "StreamDialogEntry@${tag}_playlist")
|
context.findFragmentActivity().supportFragmentManager,
|
||||||
|
"StreamDialogEntry@${tag}_playlist"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -129,7 +131,8 @@ fun StreamMenu(
|
|||||||
SparseItemUtil.fetchUploaderUrlIfSparse(
|
SparseItemUtil.fetchUploaderUrlIfSparse(
|
||||||
context, stream.serviceId, stream.url, stream.uploaderUrl
|
context, stream.serviceId, stream.url, stream.uploaderUrl
|
||||||
) { url ->
|
) { url ->
|
||||||
NavigationHelper.openChannelFragment(context.findFragmentActivity(), stream, url)
|
val activity = context.findFragmentActivity()
|
||||||
|
NavigationHelper.openChannelFragment(activity, stream, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -22,7 +22,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
|
@ -0,0 +1,278 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video.comment
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
|
import androidx.compose.material.icons.filled.ThumbUp
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
import org.schabi.newpipe.ui.components.common.rememberParsedDescription
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.copyToClipboardCallback
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showReplies by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val parsedDescription = rememberParsedDescription(comment.commentText)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.animateContentSize()
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = copyToClipboardCallback { parsedDescription },
|
||||||
|
onClick = { isExpanded = !isExpanded },
|
||||||
|
)
|
||||||
|
.padding(start = 8.dp, top = 10.dp, end = 8.dp, bottom = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
|
||||||
|
contentDescription = null,
|
||||||
|
placeholder = painterResource(R.drawable.placeholder_person),
|
||||||
|
error = painterResource(R.drawable.placeholder_person),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.size(42.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
NavigationHelper.openCommentAuthorIfPresent(context, comment)
|
||||||
|
onCommentAuthorOpened()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (comment.isPinned) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PushPin,
|
||||||
|
contentDescription = stringResource(R.string.detail_pinned_comment_view_description),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 3.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nameAndDate = remember(comment) {
|
||||||
|
val date = Localization.relativeTimeOrTextual(
|
||||||
|
context, comment.uploadDate, comment.textualUploadDate
|
||||||
|
)
|
||||||
|
Localization.concatenateStrings(comment.uploaderName, date)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = nameAndDate,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = parsedDescription,
|
||||||
|
// If the comment is expanded, we display all its content
|
||||||
|
// otherwise we only display the first two lines
|
||||||
|
maxLines = if (isExpanded) Int.MAX_VALUE else 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(top = 6.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(start = 1.dp, top = 6.dp, end = 4.dp, bottom = 6.dp)
|
||||||
|
) {
|
||||||
|
// do not show anything if the like count is unknown
|
||||||
|
if (comment.likeCount >= 0) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ThumbUp,
|
||||||
|
contentDescription = stringResource(R.string.detail_likes_img_view_description),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
.size(20.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = Localization.likeCount(context, comment.likeCount),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.isHeartedByUploader) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Favorite,
|
||||||
|
contentDescription = stringResource(R.string.detail_heart_img_view_description),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.replies != null) {
|
||||||
|
// reduce LocalMinimumInteractiveComponentSize from 48dp to 44dp to slightly
|
||||||
|
// reduce the button margin (which is still clickable but not visible)
|
||||||
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 44.dp) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { showReplies = true },
|
||||||
|
modifier = Modifier.padding(end = 2.dp)
|
||||||
|
) {
|
||||||
|
val text = pluralStringResource(
|
||||||
|
R.plurals.replies, comment.replyCount, comment.replyCount.toString()
|
||||||
|
)
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showReplies) {
|
||||||
|
CommentRepliesDialog(
|
||||||
|
parentComment = comment,
|
||||||
|
onDismissRequest = { showReplies = false },
|
||||||
|
onCommentAuthorOpened = onCommentAuthorOpened,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CommentsInfoItem(
|
||||||
|
serviceId: Int = 1,
|
||||||
|
url: String = "",
|
||||||
|
name: String = "",
|
||||||
|
commentText: Description,
|
||||||
|
uploaderName: String,
|
||||||
|
textualUploadDate: String = "5 months ago",
|
||||||
|
likeCount: Int = 0,
|
||||||
|
isHeartedByUploader: Boolean = false,
|
||||||
|
isPinned: Boolean = false,
|
||||||
|
replies: Page? = null,
|
||||||
|
replyCount: Int = 0,
|
||||||
|
) = CommentsInfoItem(serviceId, url, name).apply {
|
||||||
|
this.commentText = commentText
|
||||||
|
this.uploaderName = uploaderName
|
||||||
|
this.textualUploadDate = textualUploadDate
|
||||||
|
this.likeCount = likeCount
|
||||||
|
this.isHeartedByUploader = isHeartedByUploader
|
||||||
|
this.isPinned = isPinned
|
||||||
|
this.replies = replies
|
||||||
|
this.replyCount = replyCount
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CommentPreviewProvider : CollectionPreviewParameterProvider<CommentsInfoItem>(
|
||||||
|
listOf(
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
|
||||||
|
uploaderName = "Test",
|
||||||
|
likeCount = 100,
|
||||||
|
isPinned = false,
|
||||||
|
isHeartedByUploader = true,
|
||||||
|
replies = null,
|
||||||
|
replyCount = 0
|
||||||
|
),
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description("Hello world, long long long text lorem ipsum dolor sit amet!<br><br>This line should be hidden by default.", Description.HTML),
|
||||||
|
uploaderName = "Test",
|
||||||
|
likeCount = 92847,
|
||||||
|
isPinned = true,
|
||||||
|
isHeartedByUploader = false,
|
||||||
|
replies = Page(""),
|
||||||
|
replyCount = 10
|
||||||
|
),
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description("Hello world, long long long text lorem ipsum dolor sit amet!<br><br>This line should be hidden by default.", Description.HTML),
|
||||||
|
uploaderName = "Test really long long long long lorem ipsum dolor sit amet consectetur",
|
||||||
|
likeCount = 92847,
|
||||||
|
isPinned = true,
|
||||||
|
isHeartedByUploader = true,
|
||||||
|
replies = null,
|
||||||
|
replyCount = 0
|
||||||
|
),
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description("Short comment", Description.HTML),
|
||||||
|
uploaderName = "Test really long long long long lorem ipsum dolor sit amet consectetur",
|
||||||
|
likeCount = 92847,
|
||||||
|
isPinned = false,
|
||||||
|
isHeartedByUploader = false,
|
||||||
|
replies = Page(""),
|
||||||
|
replyCount = 4283
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun CommentPreview(
|
||||||
|
@PreviewParameter(CommentPreviewProvider::class) commentsInfoItem: CommentsInfoItem
|
||||||
|
) {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
Comment(commentsInfoItem) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun CommentListPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
Column {
|
||||||
|
for (comment in CommentPreviewProvider().values) {
|
||||||
|
Comment(comment) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video.comment
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CommentInfo(
|
||||||
|
val serviceId: Int,
|
||||||
|
val url: String,
|
||||||
|
val comments: List<CommentsInfoItem>,
|
||||||
|
val nextPage: Page?,
|
||||||
|
val commentCount: Int,
|
||||||
|
val isCommentsDisabled: Boolean
|
||||||
|
) {
|
||||||
|
constructor(commentsInfo: CommentsInfo) : this(
|
||||||
|
commentsInfo.serviceId, commentsInfo.url, commentsInfo.relatedItems, commentsInfo.nextPage,
|
||||||
|
commentsInfo.commentsCount, commentsInfo.isCommentsDisabled
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video.comment
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
import org.schabi.newpipe.paging.CommentRepliesSource
|
||||||
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
|
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommentRepliesDialog(
|
||||||
|
parentComment: CommentsInfoItem,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onCommentAuthorOpened: () -> Unit,
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val commentsFlow = remember {
|
||||||
|
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||||
|
CommentRepliesSource(parentComment)
|
||||||
|
}
|
||||||
|
.flow
|
||||||
|
.cachedIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
CommentRepliesDialog(parentComment, commentsFlow, onDismissRequest, onCommentAuthorOpened)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun CommentRepliesDialog(
|
||||||
|
parentComment: CommentsInfoItem,
|
||||||
|
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onCommentAuthorOpened: () -> Unit,
|
||||||
|
) {
|
||||||
|
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||||
|
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
val nestedOnCommentAuthorOpened: () -> Unit = {
|
||||||
|
// also partialExpand any parent dialog
|
||||||
|
onCommentAuthorOpened()
|
||||||
|
coroutineScope.launch {
|
||||||
|
sheetState.partialExpand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// contentColorFor(MaterialTheme.colorScheme.containerColor), i.e. ModalBottomSheet's
|
||||||
|
// default background color, does not resolve correctly, so need to manually set the
|
||||||
|
// content color for MaterialTheme.colorScheme.background instead
|
||||||
|
LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
LazyColumnThemedScrollbar(state = listState) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
CommentRepliesHeader(
|
||||||
|
comment = parentComment,
|
||||||
|
onCommentAuthorOpened = nestedOnCommentAuthorOpened,
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentComment.replyCount >= 0) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = 12.dp,
|
||||||
|
vertical = 4.dp
|
||||||
|
),
|
||||||
|
text = pluralStringResource(
|
||||||
|
R.plurals.replies,
|
||||||
|
parentComment.replyCount,
|
||||||
|
parentComment.replyCount,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comments.itemCount == 0) {
|
||||||
|
item {
|
||||||
|
val refresh = comments.loadState.refresh
|
||||||
|
if (refresh is LoadState.Loading) {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||||
|
} else {
|
||||||
|
val message = if (refresh is LoadState.Error) {
|
||||||
|
R.string.error_unable_to_load_comments
|
||||||
|
} else {
|
||||||
|
R.string.no_comments
|
||||||
|
}
|
||||||
|
NoItemsMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(comments.itemCount) {
|
||||||
|
Comment(
|
||||||
|
comment = comments[it]!!,
|
||||||
|
onCommentAuthorOpened = nestedOnCommentAuthorOpened,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun CommentRepliesDialogPreview() {
|
||||||
|
val comment = CommentsInfoItem(
|
||||||
|
commentText = Description("Hello world!", Description.PLAIN_TEXT),
|
||||||
|
uploaderName = "Test",
|
||||||
|
likeCount = 100,
|
||||||
|
isPinned = true,
|
||||||
|
isHeartedByUploader = true
|
||||||
|
)
|
||||||
|
val replies = (1..10).map { i ->
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description(
|
||||||
|
"Reply $i: ${LoremIpsum(i * i).values.first()}",
|
||||||
|
Description.PLAIN_TEXT,
|
||||||
|
),
|
||||||
|
uploaderName = LoremIpsum(11 - i).values.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val flow = flowOf(PagingData.from(replies))
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
CommentRepliesDialog(comment, flow, onDismissRequest = {}, onCommentAuthorOpened = {})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video.comment
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
|
import androidx.compose.material.icons.filled.ThumbUp
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
import org.schabi.newpipe.ui.components.common.DescriptionText
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
NavigationHelper.openCommentAuthorIfPresent(context, comment)
|
||||||
|
onCommentAuthorOpened()
|
||||||
|
}
|
||||||
|
.weight(1.0f, true),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
|
||||||
|
contentDescription = null,
|
||||||
|
placeholder = painterResource(R.drawable.placeholder_person),
|
||||||
|
error = painterResource(R.drawable.placeholder_person),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(42.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = comment.uploaderName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = Localization.relativeTimeOrTextual(
|
||||||
|
context, comment.uploadDate, comment.textualUploadDate
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// do not show anything if the like count is unknown
|
||||||
|
if (comment.likeCount >= 0) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ThumbUp,
|
||||||
|
contentDescription = stringResource(R.string.detail_likes_img_view_description),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = Localization.likeCount(context, comment.likeCount),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.isHeartedByUploader) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Favorite,
|
||||||
|
contentDescription = stringResource(R.string.detail_heart_img_view_description),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.isPinned) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PushPin,
|
||||||
|
contentDescription = stringResource(R.string.detail_pinned_comment_view_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DescriptionText(
|
||||||
|
description = comment.commentText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun CommentRepliesHeaderPreview() {
|
||||||
|
val comment = CommentsInfoItem(
|
||||||
|
commentText = Description(LoremIpsum(50).values.first(), Description.PLAIN_TEXT),
|
||||||
|
uploaderName = "Test really long lorem ipsum dolor sit",
|
||||||
|
likeCount = 1000,
|
||||||
|
isPinned = true,
|
||||||
|
isHeartedByUploader = true
|
||||||
|
)
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentRepliesHeader(comment) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video.comment
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
|
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
||||||
|
import org.schabi.newpipe.viewmodels.util.Resource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
|
||||||
|
val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
CommentSection(state, commentsViewModel.comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CommentSection(
|
||||||
|
uiState: Resource<CommentInfo>,
|
||||||
|
commentsFlow: Flow<PagingData<CommentsInfoItem>>
|
||||||
|
) {
|
||||||
|
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||||
|
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||||
|
val state = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumnThemedScrollbar(state = state) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||||
|
state = state
|
||||||
|
) {
|
||||||
|
when (uiState) {
|
||||||
|
is Resource.Loading -> {
|
||||||
|
item {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Success -> {
|
||||||
|
val commentInfo = uiState.data
|
||||||
|
val count = commentInfo.commentCount
|
||||||
|
|
||||||
|
if (commentInfo.isCommentsDisabled) {
|
||||||
|
item {
|
||||||
|
NoItemsMessage(R.string.comments_are_disabled)
|
||||||
|
}
|
||||||
|
} else if (count == 0) {
|
||||||
|
item {
|
||||||
|
NoItemsMessage(R.string.no_comments)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// do not show anything if the comment count is unknown
|
||||||
|
if (count >= 0) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 12.dp, end = 12.dp, bottom = 4.dp),
|
||||||
|
text = pluralStringResource(R.plurals.comments, count, count),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (comments.loadState.refresh) {
|
||||||
|
is LoadState.Loading -> {
|
||||||
|
item {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is LoadState.Error -> {
|
||||||
|
item {
|
||||||
|
NoItemsMessage(R.string.error_unable_to_load_comments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
items(comments.itemCount) {
|
||||||
|
Comment(comment = comments[it]!!) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Error -> {
|
||||||
|
item {
|
||||||
|
NoItemsMessage(R.string.error_unable_to_load_comments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun CommentSectionLoadingPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun CommentSectionSuccessPreview() {
|
||||||
|
val comments = listOf(
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description(
|
||||||
|
"Comment 1\n\nThis line should be hidden by default.",
|
||||||
|
Description.PLAIN_TEXT
|
||||||
|
),
|
||||||
|
uploaderName = "Test",
|
||||||
|
replies = Page(""),
|
||||||
|
replyCount = 10
|
||||||
|
)
|
||||||
|
) + (2..10).map {
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
||||||
|
uploaderName = "Test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(
|
||||||
|
uiState = Resource.Success(
|
||||||
|
CommentInfo(
|
||||||
|
serviceId = 1, url = "", comments = comments, nextPage = null,
|
||||||
|
commentCount = 10, isCommentsDisabled = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
commentsFlow = flowOf(PagingData.from(comments))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun CommentSectionErrorPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.evernote.android.state.StateSaver;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
import com.livefront.bridge.SavedStateHandler;
|
||||||
|
import com.livefront.bridge.ViewSavedStateHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures Bridge's state saver.
|
||||||
|
*/
|
||||||
|
public final class BridgeStateSaverInitializer {
|
||||||
|
|
||||||
|
public static void init(final Context context) {
|
||||||
|
Bridge.initialize(
|
||||||
|
context,
|
||||||
|
new SavedStateHandler() {
|
||||||
|
@Override
|
||||||
|
public void saveInstanceState(
|
||||||
|
@NonNull final Object target,
|
||||||
|
@NonNull final Bundle state) {
|
||||||
|
StateSaver.saveInstanceState(target, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restoreInstanceState(
|
||||||
|
@NonNull final Object target,
|
||||||
|
@Nullable final Bundle state) {
|
||||||
|
StateSaver.restoreInstanceState(target, state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new ViewSavedStateHandler() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public <T extends View> Parcelable saveInstanceState(
|
||||||
|
@NonNull final T target,
|
||||||
|
@Nullable final Parcelable parentState) {
|
||||||
|
return StateSaver.saveInstanceState(target, parentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <T extends View> Parcelable restoreInstanceState(
|
||||||
|
@NonNull final T target,
|
||||||
|
@Nullable final Parcelable state) {
|
||||||
|
return StateSaver.restoreInstanceState(target, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BridgeStateSaverInitializer() {
|
||||||
|
}
|
||||||
|
}
|
@ -130,7 +130,7 @@ public final class DeviceUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isFireTV =
|
isFireTV =
|
||||||
App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
|
App.getInstance().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
|
||||||
return isFireTV;
|
return isFireTV;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ public final class DeviceUtils {
|
|||||||
return isTV;
|
return isTV;
|
||||||
}
|
}
|
||||||
|
|
||||||
final PackageManager pm = App.getApp().getPackageManager();
|
final PackageManager pm = App.getInstance().getPackageManager();
|
||||||
|
|
||||||
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
|
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
|
||||||
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)
|
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)
|
||||||
|
@ -42,8 +42,6 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||||||
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.tabs.ChannelTabInfo;
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
@ -146,33 +144,6 @@ public final class ExtractorHelper {
|
|||||||
listLinkHandler, nextPage));
|
listLinkHandler, nextPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
|
||||||
final String url,
|
|
||||||
final boolean forceLoad) {
|
|
||||||
checkServiceId(serviceId);
|
|
||||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.COMMENTS,
|
|
||||||
Single.fromCallable(() ->
|
|
||||||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
|
||||||
final int serviceId,
|
|
||||||
final CommentsInfo info,
|
|
||||||
final Page nextPage) {
|
|
||||||
checkServiceId(serviceId);
|
|
||||||
return Single.fromCallable(() ->
|
|
||||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
|
||||||
final int serviceId,
|
|
||||||
final String url,
|
|
||||||
final Page nextPage) {
|
|
||||||
checkServiceId(serviceId);
|
|
||||||
return Single.fromCallable(() ->
|
|
||||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||||
final String url,
|
final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
|
@ -219,11 +219,6 @@ public final class Localization {
|
|||||||
deletedCount, shortCount(context, deletedCount));
|
deletedCount, shortCount(context, deletedCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String replyCount(@NonNull final Context context, final int replyCount) {
|
|
||||||
return getQuantity(context, R.plurals.replies, 0, replyCount,
|
|
||||||
String.valueOf(replyCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context the Android context
|
* @param context the Android context
|
||||||
* @param likeCount the like count, possibly negative if unknown
|
* @param likeCount the like count, possibly negative if unknown
|
||||||
|
@ -21,6 +21,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||||
|
|
||||||
@ -45,10 +46,10 @@ 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;
|
||||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
|
import org.schabi.newpipe.ktx.ContextKt;
|
||||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||||
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
||||||
@ -64,6 +65,7 @@ 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.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
|
import org.schabi.newpipe.settings.SettingsV2Activity;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -484,31 +486,23 @@ public final class NavigationHelper {
|
|||||||
* Opens the comment author channel fragment, if the {@link CommentsInfoItem#getUploaderUrl()}
|
* Opens the comment author channel fragment, if the {@link CommentsInfoItem#getUploaderUrl()}
|
||||||
* of {@code comment} is non-null. Shows a UI-error snackbar if something goes wrong.
|
* of {@code comment} is non-null. Shows a UI-error snackbar if something goes wrong.
|
||||||
*
|
*
|
||||||
* @param activity the activity with the fragment manager and in which to show the snackbar
|
* @param context the context to use for opening the fragment
|
||||||
* @param comment the comment whose uploader/author will be opened
|
* @param comment the comment whose uploader/author will be opened
|
||||||
*/
|
*/
|
||||||
public static void openCommentAuthorIfPresent(@NonNull final FragmentActivity activity,
|
public static void openCommentAuthorIfPresent(@NonNull final Context context,
|
||||||
@NonNull final CommentsInfoItem comment) {
|
@NonNull final CommentsInfoItem comment) {
|
||||||
if (isEmpty(comment.getUploaderUrl())) {
|
if (isEmpty(comment.getUploaderUrl())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
final var activity = ContextKt.findFragmentActivity(context);
|
||||||
openChannelFragment(activity.getSupportFragmentManager(), comment.getServiceId(),
|
openChannelFragment(activity.getSupportFragmentManager(), comment.getServiceId(),
|
||||||
comment.getUploaderUrl(), comment.getUploaderName());
|
comment.getUploaderUrl(), comment.getUploaderName());
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
|
|
||||||
@NonNull final CommentsInfoItem comment) {
|
|
||||||
defaultTransaction(activity.getSupportFragmentManager())
|
|
||||||
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
|
|
||||||
CommentRepliesFragment.TAG)
|
|
||||||
.addToBackStack(CommentRepliesFragment.TAG)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
@ -648,7 +642,13 @@ public final class NavigationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void openSettings(final Context context) {
|
public static void openSettings(final Context context) {
|
||||||
final Intent intent = new Intent(context, SettingsActivity.class);
|
final Class<?> settingsClass = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(
|
||||||
|
ContextCompat.getString(context, R.string.settings_layout_redesign_key),
|
||||||
|
false
|
||||||
|
) ? SettingsV2Activity.class : SettingsActivity.class;
|
||||||
|
|
||||||
|
final Intent intent = new Intent(context, settingsClass);
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ object ReleaseVersionUtil {
|
|||||||
val certificates = mapOf(
|
val certificates = mapOf(
|
||||||
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
|
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
|
||||||
)
|
)
|
||||||
val app = App.getApp()
|
val app = App.instance
|
||||||
try {
|
try {
|
||||||
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
|
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.schabi.newpipe.util.external_communication;
|
package org.schabi.newpipe.util.external_communication;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
import static coil3.Image_androidKt.toBitmap;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
@ -31,9 +32,9 @@ import java.nio.file.Files;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import coil.Coil;
|
import coil3.SingletonImageLoader;
|
||||||
import coil.disk.DiskCache;
|
import coil3.disk.DiskCache;
|
||||||
import coil.memory.MemoryCache;
|
import coil3.memory.MemoryCache;
|
||||||
|
|
||||||
public final class ShareUtils {
|
public final class ShareUtils {
|
||||||
private static final String TAG = ShareUtils.class.getSimpleName();
|
private static final String TAG = ShareUtils.class.getSimpleName();
|
||||||
@ -377,13 +378,13 @@ public final class ShareUtils {
|
|||||||
// Save the image in memory to the application's cache because we need a URI to the
|
// Save the image in memory to the application's cache because we need a URI to the
|
||||||
// image to generate a ClipData which will show the share sheet, and so an image file
|
// image to generate a ClipData which will show the share sheet, and so an image file
|
||||||
final Context applicationContext = context.getApplicationContext();
|
final Context applicationContext = context.getApplicationContext();
|
||||||
final var loader = Coil.imageLoader(context);
|
final var loader = SingletonImageLoader.get(context);
|
||||||
final var value = loader.getMemoryCache()
|
final var value = loader.getMemoryCache()
|
||||||
.get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap()));
|
.get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap()));
|
||||||
|
|
||||||
final Bitmap cachedBitmap;
|
final Bitmap cachedBitmap;
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
cachedBitmap = value.getBitmap();
|
cachedBitmap = toBitmap(value.getImage());
|
||||||
} else {
|
} else {
|
||||||
try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) {
|
try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) {
|
||||||
if (snapshot != null) {
|
if (snapshot != null) {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.schabi.newpipe.util.external_communication
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
|
||||||
|
fun ClipboardManager.setTextAndShowToast(context: Context, annotatedString: AnnotatedString) {
|
||||||
|
setText(annotatedString)
|
||||||
|
if (Build.VERSION.SDK_INT < 33) {
|
||||||
|
// Android 13 has its own "copied to clipboard" dialog
|
||||||
|
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun copyToClipboardCallback(annotatedString: () -> AnnotatedString): (() -> Unit) {
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
return {
|
||||||
|
clipboardManager.setTextAndShowToast(context, annotatedString())
|
||||||
|
}
|
||||||
|
}
|
@ -5,14 +5,18 @@ import android.graphics.Bitmap
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
import coil3.executeBlocking
|
||||||
import coil.executeBlocking
|
import coil3.imageLoader
|
||||||
import coil.imageLoader
|
import coil3.request.Disposable
|
||||||
import coil.request.Disposable
|
import coil3.request.ImageRequest
|
||||||
import coil.request.ImageRequest
|
import coil3.request.error
|
||||||
import coil.size.Size
|
import coil3.request.placeholder
|
||||||
import coil.target.Target
|
import coil3.request.target
|
||||||
import coil.transform.Transformation
|
import coil3.request.transformations
|
||||||
|
import coil3.size.Size
|
||||||
|
import coil3.target.Target
|
||||||
|
import coil3.toBitmap
|
||||||
|
import coil3.transform.Transformation
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.Image
|
import org.schabi.newpipe.extractor.Image
|
||||||
@ -26,84 +30,119 @@ object CoilHelper {
|
|||||||
fun loadBitmapBlocking(
|
fun loadBitmapBlocking(
|
||||||
context: Context,
|
context: Context,
|
||||||
url: String?,
|
url: String?,
|
||||||
@DrawableRes placeholderResId: Int = 0
|
@DrawableRes placeholderResId: Int = 0,
|
||||||
): Bitmap? {
|
): Bitmap? =
|
||||||
val request = getImageRequest(context, url, placeholderResId).build()
|
context.imageLoader
|
||||||
return context.imageLoader.executeBlocking(request).drawable?.toBitmapOrNull()
|
.executeBlocking(getImageRequest(context, url, placeholderResId).build())
|
||||||
}
|
.image
|
||||||
|
?.toBitmap()
|
||||||
|
|
||||||
fun loadAvatar(target: ImageView, images: List<Image>) {
|
fun loadAvatar(
|
||||||
|
target: ImageView,
|
||||||
|
images: List<Image>,
|
||||||
|
) {
|
||||||
loadImageDefault(target, images, R.drawable.placeholder_person)
|
loadImageDefault(target, images, R.drawable.placeholder_person)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAvatar(target: ImageView, url: String?) {
|
fun loadAvatar(
|
||||||
|
target: ImageView,
|
||||||
|
url: String?,
|
||||||
|
) {
|
||||||
loadImageDefault(target, url, R.drawable.placeholder_person)
|
loadImageDefault(target, url, R.drawable.placeholder_person)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadThumbnail(target: ImageView, images: List<Image>) {
|
fun loadThumbnail(
|
||||||
|
target: ImageView,
|
||||||
|
images: List<Image>,
|
||||||
|
) {
|
||||||
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video)
|
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadThumbnail(target: ImageView, url: String?) {
|
fun loadThumbnail(
|
||||||
|
target: ImageView,
|
||||||
|
url: String?,
|
||||||
|
) {
|
||||||
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video)
|
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadScaledDownThumbnail(context: Context, images: List<Image>, target: Target): Disposable {
|
fun loadScaledDownThumbnail(
|
||||||
|
context: Context,
|
||||||
|
images: List<Image>,
|
||||||
|
target: Target,
|
||||||
|
): Disposable {
|
||||||
val url = ImageStrategy.choosePreferredImage(images)
|
val url = ImageStrategy.choosePreferredImage(images)
|
||||||
val request = getImageRequest(context, url, R.drawable.placeholder_thumbnail_video)
|
val request =
|
||||||
.target(target)
|
getImageRequest(context, url, R.drawable.placeholder_thumbnail_video)
|
||||||
.transformations(object : Transformation {
|
.target(target)
|
||||||
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
|
.transformations(
|
||||||
|
object : Transformation() {
|
||||||
|
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
|
||||||
|
|
||||||
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
override suspend fun transform(
|
||||||
if (MainActivity.DEBUG) {
|
input: Bitmap,
|
||||||
Log.d(TAG, "Thumbnail - transform() called")
|
size: Size,
|
||||||
}
|
): Bitmap {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - transform() called")
|
||||||
|
}
|
||||||
|
|
||||||
val notificationThumbnailWidth = min(
|
val notificationThumbnailWidth =
|
||||||
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
|
min(
|
||||||
input.width.toFloat()
|
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
|
||||||
).toInt()
|
input.width.toFloat(),
|
||||||
|
).toInt()
|
||||||
|
|
||||||
var newHeight = input.height / (input.width / notificationThumbnailWidth)
|
var newHeight = input.height / (input.width / notificationThumbnailWidth)
|
||||||
val result = input.scale(notificationThumbnailWidth, newHeight)
|
val result = input.scale(notificationThumbnailWidth, newHeight)
|
||||||
|
|
||||||
return if (result == input || !result.isMutable) {
|
return if (result == input || !result.isMutable) {
|
||||||
// create a new mutable bitmap to prevent strange crashes on some
|
// create a new mutable bitmap to prevent strange crashes on some
|
||||||
// devices (see #4638)
|
// devices (see #4638)
|
||||||
newHeight = input.height / (input.width / (notificationThumbnailWidth - 1))
|
newHeight = input.height / (input.width / (notificationThumbnailWidth - 1))
|
||||||
input.scale(notificationThumbnailWidth, newHeight)
|
input.scale(notificationThumbnailWidth, newHeight)
|
||||||
} else {
|
} else {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.build()
|
).build()
|
||||||
|
|
||||||
return context.imageLoader.enqueue(request)
|
return context.imageLoader.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDetailsThumbnail(target: ImageView, images: List<Image>) {
|
fun loadDetailsThumbnail(
|
||||||
|
target: ImageView,
|
||||||
|
images: List<Image>,
|
||||||
|
) {
|
||||||
val url = ImageStrategy.choosePreferredImage(images)
|
val url = ImageStrategy.choosePreferredImage(images)
|
||||||
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false)
|
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBanner(target: ImageView, images: List<Image>) {
|
fun loadBanner(
|
||||||
|
target: ImageView,
|
||||||
|
images: List<Image>,
|
||||||
|
) {
|
||||||
loadImageDefault(target, images, R.drawable.placeholder_channel_banner)
|
loadImageDefault(target, images, R.drawable.placeholder_channel_banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadPlaylistThumbnail(target: ImageView, images: List<Image>) {
|
fun loadPlaylistThumbnail(
|
||||||
|
target: ImageView,
|
||||||
|
images: List<Image>,
|
||||||
|
) {
|
||||||
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist)
|
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadPlaylistThumbnail(target: ImageView, url: String?) {
|
fun loadPlaylistThumbnail(
|
||||||
|
target: ImageView,
|
||||||
|
url: String?,
|
||||||
|
) {
|
||||||
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist)
|
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadImageDefault(
|
private fun loadImageDefault(
|
||||||
target: ImageView,
|
target: ImageView,
|
||||||
images: List<Image>,
|
images: List<Image>,
|
||||||
@DrawableRes placeholderResId: Int
|
@DrawableRes placeholderResId: Int,
|
||||||
) {
|
) {
|
||||||
loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId)
|
loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId)
|
||||||
}
|
}
|
||||||
@ -112,11 +151,12 @@ object CoilHelper {
|
|||||||
target: ImageView,
|
target: ImageView,
|
||||||
url: String?,
|
url: String?,
|
||||||
@DrawableRes placeholderResId: Int,
|
@DrawableRes placeholderResId: Int,
|
||||||
showPlaceholder: Boolean = true
|
showPlaceholder: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val request = getImageRequest(target.context, url, placeholderResId, showPlaceholder)
|
val request =
|
||||||
.target(target)
|
getImageRequest(target.context, url, placeholderResId, showPlaceholder)
|
||||||
.build()
|
.target(target)
|
||||||
|
.build()
|
||||||
target.context.imageLoader.enqueue(request)
|
target.context.imageLoader.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,14 +164,15 @@ object CoilHelper {
|
|||||||
context: Context,
|
context: Context,
|
||||||
url: String?,
|
url: String?,
|
||||||
@DrawableRes placeholderResId: Int,
|
@DrawableRes placeholderResId: Int,
|
||||||
showPlaceholderWhileLoading: Boolean = true
|
showPlaceholderWhileLoading: Boolean = true,
|
||||||
): ImageRequest.Builder {
|
): ImageRequest.Builder {
|
||||||
// if the URL was chosen with `choosePreferredImage` it will be null, but check again
|
// if the URL was chosen with `choosePreferredImage` it will be null, but check again
|
||||||
// `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case
|
// `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case
|
||||||
// for URLs stored in the database)
|
// for URLs stored in the database)
|
||||||
val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() }
|
val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() }
|
||||||
|
|
||||||
return ImageRequest.Builder(context)
|
return ImageRequest
|
||||||
|
.Builder(context)
|
||||||
.data(takenUrl)
|
.data(takenUrl)
|
||||||
.error(placeholderResId)
|
.error(placeholderResId)
|
||||||
.memoryCacheKey(takenUrl)
|
.memoryCacheKey(takenUrl)
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.schabi.newpipe.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.paging.CommentsSource
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||||
|
import org.schabi.newpipe.util.KEY_URL
|
||||||
|
import org.schabi.newpipe.viewmodels.util.Resource
|
||||||
|
|
||||||
|
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||||
|
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
|
||||||
|
.map {
|
||||||
|
try {
|
||||||
|
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Resource.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val comments = uiState
|
||||||
|
.filterIsInstance<Resource.Success<CommentInfo>>()
|
||||||
|
.flatMapLatest {
|
||||||
|
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||||
|
CommentsSource(it.data)
|
||||||
|
}.flow
|
||||||
|
}
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.schabi.newpipe.viewmodels.util
|
||||||
|
|
||||||
|
sealed class Resource<out T> {
|
||||||
|
data object Loading : Resource<Nothing>()
|
||||||
|
class Success<T>(val data: T) : Resource<T>()
|
||||||
|
class Error(val throwable: Throwable) : Resource<Nothing>()
|
||||||
|
}
|
@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
package org.schabi.newpipe.views;
|
package org.schabi.newpipe.views;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
@ -29,18 +32,15 @@ import android.widget.LinearLayout;
|
|||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view that can be fully collapsed and expanded.
|
* A view that can be fully collapsed and expanded.
|
||||||
*/
|
*/
|
||||||
@ -207,12 +207,12 @@ public class CollapsibleView extends LinearLayout {
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Parcelable onSaveInstanceState() {
|
public Parcelable onSaveInstanceState() {
|
||||||
return Icepick.saveInstanceState(this, super.onSaveInstanceState());
|
return Bridge.saveInstanceState(this, super.onSaveInstanceState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRestoreInstanceState(final Parcelable state) {
|
public void onRestoreInstanceState(final Parcelable state) {
|
||||||
super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
|
super.onRestoreInstanceState(Bridge.restoreInstanceState(this, state));
|
||||||
|
|
||||||
ready();
|
ready();
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
../layout/list_stream_item.xml
|
|
94
app/src/main/res/layout-land/list_stream_card_item.xml
Normal file
94
app/src/main/res/layout-land/list_stream_card_item.xml
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/itemRoot"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="@dimen/video_item_search_padding">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/itemThumbnailView"
|
||||||
|
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
|
||||||
|
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="@drawable/placeholder_thumbnail_video"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/itemProgressView"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemDurationView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/video_item_search_duration_margin"
|
||||||
|
android:layout_marginBottom="@dimen/video_item_search_duration_margin"
|
||||||
|
android:background="@color/duration_background_color"
|
||||||
|
android:paddingHorizontal="@dimen/video_item_search_duration_horizontal_padding"
|
||||||
|
android:paddingVertical="@dimen/video_item_search_duration_vertical_padding"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="@color/duration_text_color"
|
||||||
|
android:textSize="@dimen/video_item_search_duration_text_size"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/itemThumbnailView"
|
||||||
|
tools:text="1:09:10" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemVideoTitleView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/video_item_search_image_right_margin"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textSize="@dimen/video_item_search_title_text_size"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/itemUploaderView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/itemThumbnailView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemUploaderView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textSize="@dimen/video_item_search_uploader_text_size"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/itemAdditionalDetails"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/itemVideoTitleView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/itemVideoTitleView"
|
||||||
|
tools:text="Uploader" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemAdditionalDetails"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/itemVideoTitleView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/itemUploaderView"
|
||||||
|
tools:text="2 years ago • 10M views" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.AnimatedProgressBar
|
||||||
|
android:id="@+id/itemProgressView"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:progressDrawable="?progress_horizontal_drawable"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/itemThumbnailView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/itemThumbnailView" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,137 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/authorAvatar"
|
|
||||||
android:layout_width="42dp"
|
|
||||||
android:layout_height="42dp"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:focusable="false"
|
|
||||||
android:src="@drawable/placeholder_person"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearance="@style/CircularImageView"
|
|
||||||
tools:ignore="RtlHardcoded" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/authorName"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/uploadDate"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
|
|
||||||
tools:text="@tools:sample/lorem/random" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/uploadDate"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:textSize="14sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/authorName"
|
|
||||||
tools:text="5 months ago" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/thumbsUpImage"
|
|
||||||
android:layout_width="21sp"
|
|
||||||
android:layout_height="21sp"
|
|
||||||
android:layout_marginEnd="@dimen/video_item_detail_like_margin"
|
|
||||||
android:contentDescription="@string/detail_likes_img_view_description"
|
|
||||||
android:src="@drawable/ic_thumb_up"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpCount"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/thumbsUpCount"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:textSize="14sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/heartImage"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
|
|
||||||
tools:text="12M" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/heartImage"
|
|
||||||
android:layout_width="21sp"
|
|
||||||
android:layout_height="21sp"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:contentDescription="@string/detail_heart_img_view_description"
|
|
||||||
android:src="@drawable/ic_heart"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/pinnedImage"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
|
|
||||||
app:layout_goneMarginEnd="16dp" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/authorTouchArea"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/commentContent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/pinnedImage"
|
|
||||||
android:layout_width="21sp"
|
|
||||||
android:layout_height="21sp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:contentDescription="@string/detail_pinned_comment_view_description"
|
|
||||||
android:src="@drawable/ic_pin"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/commentContent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
android:textSize="14sp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/authorAvatar"
|
|
||||||
tools:text="@tools:sample/lorem/random[10]" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1px"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="?attr/separator_color"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/commentContent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeRecyclerView
|
|
||||||
android:id="@+id/items_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
tools:listitem="@layout/list_comment_item" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/loading_progress_bar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/empty_state_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="85dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:fontFamily="monospace"
|
|
||||||
android:text="(╯°-°)╯"
|
|
||||||
android:textSize="35sp"
|
|
||||||
tools:ignore="HardcodedText,UnusedAttribute" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/empty_state_desc"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/empty_view_no_comments"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!--ERROR PANEL-->
|
|
||||||
<include
|
|
||||||
android:id="@+id/error_panel"
|
|
||||||
layout="@layout/error_panel"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:background="?attr/toolbar_shadow"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
@ -1,104 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/itemRoot"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:padding="@dimen/comments_vertical_padding">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/itemThumbnailView"
|
|
||||||
android:layout_width="42dp"
|
|
||||||
android:layout_height="42dp"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_marginEnd="@dimen/comment_item_avatar_right_margin"
|
|
||||||
android:focusable="false"
|
|
||||||
android:src="@drawable/placeholder_person"
|
|
||||||
app:shapeAppearance="@style/CircularImageView"
|
|
||||||
tools:ignore="RtlHardcoded" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/detail_pinned_view"
|
|
||||||
android:layout_width="@dimen/video_item_detail_pinned_image_width"
|
|
||||||
android:layout_height="@dimen/video_item_detail_pinned_image_height"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin"
|
|
||||||
android:layout_toEndOf="@+id/itemThumbnailView"
|
|
||||||
android:contentDescription="@string/detail_pinned_comment_view_description"
|
|
||||||
android:src="@drawable/ic_pin" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/itemTitleView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_toEndOf="@+id/detail_pinned_view"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:textSize="@dimen/comment_item_title_text_size"
|
|
||||||
tools:text="Author Name, Lorem ipsum • 5 months ago" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/itemCommentContentView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/itemTitleView"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:layout_toEndOf="@+id/itemThumbnailView"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
android:textSize="@dimen/comment_item_content_text_size"
|
|
||||||
tools:text="@tools:sample/lorem/random[1]" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/detail_thumbs_up_img_view"
|
|
||||||
android:layout_width="@dimen/video_item_detail_like_image_width"
|
|
||||||
android:layout_height="@dimen/video_item_detail_like_image_height"
|
|
||||||
android:layout_below="@id/itemCommentContentView"
|
|
||||||
android:layout_alignBottom="@+id/replies_button"
|
|
||||||
android:layout_toEndOf="@+id/itemThumbnailView"
|
|
||||||
android:contentDescription="@string/detail_likes_img_view_description"
|
|
||||||
android:src="@drawable/ic_thumb_up" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/detail_thumbs_up_count_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignTop="@id/detail_thumbs_up_img_view"
|
|
||||||
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
|
|
||||||
android:layout_marginStart="@dimen/video_item_detail_like_margin"
|
|
||||||
android:layout_toEndOf="@id/detail_thumbs_up_img_view"
|
|
||||||
android:gravity="center"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:textSize="@dimen/video_item_detail_likes_text_size"
|
|
||||||
tools:text="12M" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/detail_heart_image_view"
|
|
||||||
android:layout_width="@dimen/video_item_detail_heart_image_size"
|
|
||||||
android:layout_height="@dimen/video_item_detail_heart_image_size"
|
|
||||||
android:layout_alignTop="@id/detail_thumbs_up_img_view"
|
|
||||||
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
|
|
||||||
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
|
|
||||||
android:layout_toEndOf="@+id/detail_thumbs_up_count_view"
|
|
||||||
android:contentDescription="@string/detail_heart_img_view_description"
|
|
||||||
android:src="@drawable/ic_heart" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/replies_button"
|
|
||||||
style="?android:attr/borderlessButtonStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/itemCommentContentView"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
|
|
||||||
android:minHeight="0dp"
|
|
||||||
tools:text="543 replies" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
@ -246,6 +246,7 @@
|
|||||||
<string name="crash_the_app_key">crash_the_app_key</string>
|
<string name="crash_the_app_key">crash_the_app_key</string>
|
||||||
<string name="show_error_snackbar_key">show_error_snackbar_key</string>
|
<string name="show_error_snackbar_key">show_error_snackbar_key</string>
|
||||||
<string name="create_error_notification_key">create_error_notification_key</string>
|
<string name="create_error_notification_key">create_error_notification_key</string>
|
||||||
|
<string name="settings_layout_redesign_key">settings_layout_redesign_key</string>
|
||||||
|
|
||||||
<!-- THEMES -->
|
<!-- THEMES -->
|
||||||
<string name="theme_key">theme</string>
|
<string name="theme_key">theme</string>
|
||||||
|
@ -492,6 +492,7 @@
|
|||||||
<string name="crash_the_app">Crash the app</string>
|
<string name="crash_the_app">Crash the app</string>
|
||||||
<string name="show_error_snackbar">Show an error snackbar</string>
|
<string name="show_error_snackbar">Show an error snackbar</string>
|
||||||
<string name="create_error_notification">Create an error notification</string>
|
<string name="create_error_notification">Create an error notification</string>
|
||||||
|
<string name="settings_layout_redesign">Enable the Redesigned Settings page</string>
|
||||||
<!-- Subscriptions import/export -->
|
<!-- Subscriptions import/export -->
|
||||||
<string name="import_title">Import</string>
|
<string name="import_title">Import</string>
|
||||||
<string name="import_from">Import from</string>
|
<string name="import_from">Import from</string>
|
||||||
@ -856,4 +857,8 @@
|
|||||||
<string name="show_less">Show less</string>
|
<string name="show_less">Show less</string>
|
||||||
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
|
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
|
||||||
<string name="auto_queue_description">Next</string>
|
<string name="auto_queue_description">Next</string>
|
||||||
|
<plurals name="comments">
|
||||||
|
<item quantity="one">%d comment</item>
|
||||||
|
<item quantity="other">%d comments</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -64,4 +64,11 @@
|
|||||||
android:title="@string/create_error_notification"
|
android:title="@string/create_error_notification"
|
||||||
app:singleLineTitle="false"
|
app:singleLineTitle="false"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="@string/settings_layout_redesign_key"
|
||||||
|
android:title="@string/settings_layout_redesign"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:singleLineTitle="false" />
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
package org.schabi.newpipe.error;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.RouterActivity;
|
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for {@link ErrorActivity}.
|
|
||||||
*/
|
|
||||||
public class ErrorActivityTest {
|
|
||||||
@Test
|
|
||||||
public void getReturnActivity() {
|
|
||||||
Class<? extends Activity> returnActivity;
|
|
||||||
returnActivity = ErrorActivity.getReturnActivity(MainActivity.class);
|
|
||||||
assertEquals(MainActivity.class, returnActivity);
|
|
||||||
|
|
||||||
returnActivity = ErrorActivity.getReturnActivity(RouterActivity.class);
|
|
||||||
assertEquals(RouterActivity.class, returnActivity);
|
|
||||||
|
|
||||||
returnActivity = ErrorActivity.getReturnActivity(null);
|
|
||||||
assertNull(returnActivity);
|
|
||||||
|
|
||||||
returnActivity = ErrorActivity.getReturnActivity(Integer.class);
|
|
||||||
assertEquals(MainActivity.class, returnActivity);
|
|
||||||
|
|
||||||
returnActivity = ErrorActivity.getReturnActivity(VideoDetailFragment.class);
|
|
||||||
assertEquals(MainActivity.class, returnActivity);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,15 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '2.0.0'
|
ext.kotlin_version = '2.0.21'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.2.0'
|
classpath 'com.android.tools.build:gradle:8.7.1'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
@ -106,7 +106,7 @@ NewPipe এ আপুনি ব্যৱহাৰ কৰা সেৱাৰ অ
|
|||||||
|
|
||||||
## অৱদান
|
## অৱদান
|
||||||
|
|
||||||
আপোনাৰ ধাৰণা, অনুবাদ, ডিজাইন পৰিবৰ্তন, ক'ড পৰিষ্কাৰ কৰা, বা আনকি ডাঙৰ ক'ড পৰিৱৰ্তন হওক, সহায় সদায় আদৰণীয়। প্ৰতিটো অৱদানৰ লগে লগে এপটো ভাল হৈ পৰে, যিমানেই ডাঙৰ বা সৰু নহওক কিয়! যদি আপুনি জড়িত হ'ব বিচাৰে তেন্তে চাওক আমাৰ [অবদানৰ টোকা সমূহ](.github/CONTRIBUTING.md).<a href="https://hosted.weblate.org/engage/newpipe/"><img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" /></a>
|
আপোনাৰ ধাৰণা, অনুবাদ, ডিজাইন পৰিবৰ্তন, ক'ড পৰিষ্কাৰ কৰা, বা আনকি ডাঙৰ ক'ড পৰিৱৰ্তন হওক, সহায় সদায় আদৰণীয়। প্ৰতিটো অৱদানৰ লগে লগে এপটো ভাল হৈ পৰে, যিমানেই ডাঙৰ বা সৰু নহওক কিয়! যদি আপুনি জড়িত হ'ব বিচাৰে তেন্তে চাওক আমাৰ [অবদানৰ টোকা সমূহ](/.github/CONTRIBUTING.md).<a href="https://hosted.weblate.org/engage/newpipe/"><img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" /></a>
|
||||||
|
|
||||||
## অনুদান
|
## অনুদান
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ So eine Aktion wird nicht unterstützt und du solltest sie nur in Erwägung zieh
|
|||||||
## Beitrag
|
## Beitrag
|
||||||
Egal ob du neue Ideen, Übersetzungen, Designvorschläge, kleine Code-Bereinigungen, oder sogar große Code-Verbesserungen hast, jegliche Unterstützung ist immer gern gesehen.
|
Egal ob du neue Ideen, Übersetzungen, Designvorschläge, kleine Code-Bereinigungen, oder sogar große Code-Verbesserungen hast, jegliche Unterstützung ist immer gern gesehen.
|
||||||
Die App wird mit _jedem_ Beitrag besser und besser - egal wie viel Arbeit in ihn gesteckt wird!
|
Die App wird mit _jedem_ Beitrag besser und besser - egal wie viel Arbeit in ihn gesteckt wird!
|
||||||
Wenn du dich einbringen willst, sehe dir die [Beitragshinweise](.github/CONTRIBUTING.md) an.
|
Wenn du dich einbringen willst, sieh dir die [Beitragshinweise](/.github/CONTRIBUTING.md) an.
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/de/">
|
<a href="https://hosted.weblate.org/engage/newpipe/de/">
|
||||||
<img src="https://hosted.weblate.org/widgets/newpipe/de/287x66-grey.png" alt="Übersetzt" />
|
<img src="https://hosted.weblate.org/widgets/newpipe/de/287x66-grey.png" alt="Übersetzt" />
|
||||||
|
@ -109,7 +109,7 @@ Entre temps, si vous voulez changer de source pour une raison quelconque (par ex
|
|||||||
|
|
||||||
## Contribuer
|
## Contribuer
|
||||||
|
|
||||||
Que vous ayez des idées, des traductions, des changements de design, du nettoyage de code, ou encore un changement de code majeur, toute aide est la bienvenue. L'app s'améliore un peu plus à chaque contribution, peu importe qu'elle soit grosse ou petite ! Si vous aimeriez être impliqué, jetez un coup d'oeil à nos [notes pour contribuer](.github/CONTRIBUTING.md).
|
Que vous ayez des idées, des traductions, des changements de design, du nettoyage de code, ou encore un changement de code majeur, toute aide est la bienvenue. L'app s'améliore un peu plus à chaque contribution, peu importe qu'elle soit grosse ou petite ! Si vous aimeriez être impliqué, jetez un coup d'oeil à nos [notes pour contribuer](/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||||
|
@ -105,7 +105,7 @@ NewPipe पर कई सेवाएँ उपलब्ध हैं। हम
|
|||||||
चाहे आप अपने विचार जोड़ना चाहे, या अनुवाद, डिज़ाइन में बदलाव, कोड में सफ़ाई, या कोड में भारी बदलाव, सहायता ज़रूर करें।
|
चाहे आप अपने विचार जोड़ना चाहे, या अनुवाद, डिज़ाइन में बदलाव, कोड में सफ़ाई, या कोड में भारी बदलाव, सहायता ज़रूर करें।
|
||||||
जितने योगदान हो, ऐप उतनी ही बेहतर होती जाती है!
|
जितने योगदान हो, ऐप उतनी ही बेहतर होती जाती है!
|
||||||
|
|
||||||
अगर आप योगदान करना चाहते हैं, हमारे [योगदान के दिशानिर्देश](.github/CONTRIBUTING.md) देखें।
|
अगर आप योगदान करना चाहते हैं, हमारे [योगदान के दिशानिर्देश](/.github/CONTRIBUTING.md) देखें।
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="अनुवाद की स्थिति" />
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="अनुवाद की स्थिति" />
|
||||||
|
@ -107,7 +107,7 @@ Nel frattempo, se vuoi cambiare fonte per la stessa ragione (ad es. la funzional
|
|||||||
|
|
||||||
## Contribuire
|
## Contribuire
|
||||||
|
|
||||||
Se hai idee, traduzioni, cambiamenti di *design*, pulizia di codice, o addirittura grossi cambiamenti di codice, l'aiuto è sempre apprezzato. L'app diventa sempre meglio con ogni contribuzione, non importa quanto grande o piccola essa sia! Se ti piacerebbe essere parte del progetto, vedi le nostre [note di contribuzione](.github/CONTRIBUTING.md).
|
Se hai idee, traduzioni, cambiamenti di *design*, pulizia di codice, o addirittura grossi cambiamenti di codice, l'aiuto è sempre apprezzato. L'app diventa sempre meglio con ogni contribuzione, non importa quanto grande o piccola essa sia! Se ti piacerebbe essere parte del progetto, vedi le nostre [note di contribuzione](/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Stato traduzione" />
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Stato traduzione" />
|
||||||
|
@ -105,7 +105,7 @@ NewPipe ਤੁਹਾਡੇ ਦੁਆਰਾ ਵਰਤੀ ਜਾ ਰਹੀ ਸੇ
|
|||||||
<b>ਨੋਟ: ਜਦੋਂ ਤੁਸੀਂ ਅਧਿਕਾਰਤ ਐਪ ਵਿੱਚ ਇੱਕ ਡੇਟਾਬੇਸ ਨੂੰ ਆਯਾਤ ਕਰ ਰਹੇ ਹੋ, ਤਾਂ ਹਮੇਸ਼ਾਂ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਇਹ ਉਹੀ ਹੈ ਜੋ ਤੁਸੀਂ ਅਧਿਕਾਰਤ ਐਪ ਤੋਂ ਨਿਰਯਾਤ ਕੀਤਾ ਹੈ। ਜੇਕਰ ਤੁਸੀਂ ਅਧਿਕਾਰਤ ਐਪ ਤੋਂ ਇਲਾਵਾ ਕਿਸੇ ਏਪੀਕੇ ਤੋਂ ਨਿਰਯਾਤ ਕੀਤੇ ਡੇਟਾਬੇਸ ਨੂੰ ਆਯਾਤ ਕਰਦੇ ਹੋ, ਤਾਂ ਇਹ ਚੀਜ਼ਾਂ ਨੂੰ ਤੋੜ ਸਕਦਾ ਹੈ। ਅਜਿਹੀ ਕਾਰਵਾਈ ਅਸਮਰਥਿਤ ਹੈ, ਅਤੇ ਤੁਹਾਨੂੰ ਅਜਿਹਾ ਉਦੋਂ ਹੀ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ ਜਦੋਂ ਤੁਹਾਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਯਕੀਨ ਹੋਵੇ ਕਿ ਤੁਸੀਂ ਜਾਣਦੇ ਹੋ ਕਿ ਤੁਸੀਂ ਕੀ ਕਰ ਰਹੇ ਹੋ।</b>
|
<b>ਨੋਟ: ਜਦੋਂ ਤੁਸੀਂ ਅਧਿਕਾਰਤ ਐਪ ਵਿੱਚ ਇੱਕ ਡੇਟਾਬੇਸ ਨੂੰ ਆਯਾਤ ਕਰ ਰਹੇ ਹੋ, ਤਾਂ ਹਮੇਸ਼ਾਂ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਇਹ ਉਹੀ ਹੈ ਜੋ ਤੁਸੀਂ ਅਧਿਕਾਰਤ ਐਪ ਤੋਂ ਨਿਰਯਾਤ ਕੀਤਾ ਹੈ। ਜੇਕਰ ਤੁਸੀਂ ਅਧਿਕਾਰਤ ਐਪ ਤੋਂ ਇਲਾਵਾ ਕਿਸੇ ਏਪੀਕੇ ਤੋਂ ਨਿਰਯਾਤ ਕੀਤੇ ਡੇਟਾਬੇਸ ਨੂੰ ਆਯਾਤ ਕਰਦੇ ਹੋ, ਤਾਂ ਇਹ ਚੀਜ਼ਾਂ ਨੂੰ ਤੋੜ ਸਕਦਾ ਹੈ। ਅਜਿਹੀ ਕਾਰਵਾਈ ਅਸਮਰਥਿਤ ਹੈ, ਅਤੇ ਤੁਹਾਨੂੰ ਅਜਿਹਾ ਉਦੋਂ ਹੀ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ ਜਦੋਂ ਤੁਹਾਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਯਕੀਨ ਹੋਵੇ ਕਿ ਤੁਸੀਂ ਜਾਣਦੇ ਹੋ ਕਿ ਤੁਸੀਂ ਕੀ ਕਰ ਰਹੇ ਹੋ।</b>
|
||||||
|
|
||||||
## ਯੋਗਦਾਨ
|
## ਯੋਗਦਾਨ
|
||||||
ਭਾਵੇਂ ਤੁਹਾਡੇ ਕੋਲ ਵਿਚਾਰ, ਅਨੁਵਾਦ, ਡਿਜ਼ਾਈਨ ਤਬਦੀਲੀਆਂ, ਕੋਡ ਦੀ ਸਫਾਈ, ਜਾਂ ਇੱਥੋਂ ਤੱਕ ਕਿ ਵੱਡੀਆਂ ਕੋਡ ਤਬਦੀਲੀਆਂ ਹੋਣ, ਮਦਦ ਦਾ ਹਮੇਸ਼ਾ ਸਵਾਗਤ ਹੈ। ਐਪ ਹਰੇਕ ਯੋਗਦਾਨ ਦੇ ਨਾਲ ਬਿਹਤਰ ਅਤੇ ਬਿਹਤਰ ਹੋ ਜਾਂਦੀ ਹੈ, ਚਾਹੇ ਉਹ ਕਿੰਨਾ ਵੱਡਾ ਜਾਂ ਛੋਟਾ ਹੋਵੇ! ਜੇਕਰ ਤੁਸੀਂ ਸ਼ਾਮਲ ਹੋਣਾ ਚਾਹੁੰਦੇ ਹੋ, ਤਾਂ ਸਾਡੀ ਜਾਂਚ ਕਰੋ [contribution notes](.github/CONTRIBUTING.md).
|
ਭਾਵੇਂ ਤੁਹਾਡੇ ਕੋਲ ਵਿਚਾਰ, ਅਨੁਵਾਦ, ਡਿਜ਼ਾਈਨ ਤਬਦੀਲੀਆਂ, ਕੋਡ ਦੀ ਸਫਾਈ, ਜਾਂ ਇੱਥੋਂ ਤੱਕ ਕਿ ਵੱਡੀਆਂ ਕੋਡ ਤਬਦੀਲੀਆਂ ਹੋਣ, ਮਦਦ ਦਾ ਹਮੇਸ਼ਾ ਸਵਾਗਤ ਹੈ। ਐਪ ਹਰੇਕ ਯੋਗਦਾਨ ਦੇ ਨਾਲ ਬਿਹਤਰ ਅਤੇ ਬਿਹਤਰ ਹੋ ਜਾਂਦੀ ਹੈ, ਚਾਹੇ ਉਹ ਕਿੰਨਾ ਵੱਡਾ ਜਾਂ ਛੋਟਾ ਹੋਵੇ! ਜੇਕਰ ਤੁਸੀਂ ਸ਼ਾਮਲ ਹੋਣਾ ਚਾਹੁੰਦੇ ਹੋ, ਤਾਂ ਸਾਡੀ ਜਾਂਚ ਕਰੋ [contribution notes](/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||||
|
@ -106,7 +106,7 @@ Enquanto isso, se você quiser trocar de fontes por algum motivo (por exemplo, a
|
|||||||
4. Importe os dados da etapa 1 via Configurações > Backup e Restauração > Importar Base de Dados
|
4. Importe os dados da etapa 1 via Configurações > Backup e Restauração > Importar Base de Dados
|
||||||
|
|
||||||
## Contribuições
|
## Contribuições
|
||||||
Se você tem ideias, traduções, alterações de design, limpeza de códigos ou mudanças reais de código, a ajuda é sempre bem-vinda. O aplicativo fica cada vez melhor a cada contribuição, não importa quão grande ou pequena! Se você quiser se envolver, verifique nossas [notas de contribuição](.github/CONTRIBUTING.md).
|
Se você tem ideias, traduções, alterações de design, limpeza de códigos ou mudanças reais de código, a ajuda é sempre bem-vinda. O aplicativo fica cada vez melhor a cada contribuição, não importa quão grande ou pequena! Se você quiser se envolver, verifique nossas [notas de contribuição](/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Estado da tradução" />
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Estado da tradução" />
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user