mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-11-22 10:15:16 +01:00
Compare commits
18 Commits
7df62000a9
...
e3f44faa10
Author | SHA1 | Date | |
---|---|---|---|
|
e3f44faa10 | ||
|
cff3834fde | ||
|
c8b01a06b0 | ||
|
414b1a8344 | ||
|
404d9f3fac | ||
|
55e4014036 | ||
|
1cd5563b27 | ||
|
1abced992b | ||
|
46b9243661 | ||
|
ad72b2cb31 | ||
|
8b79d0ee29 | ||
|
295f719b77 | ||
|
b584353f4d | ||
|
d73314b4dd | ||
|
9f4a33c7a8 | ||
|
3a9540b042 | ||
|
ca855cbca0 | ||
|
6a98b1dac7 |
@ -124,6 +124,8 @@ ext {
|
|||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
leakCanaryVersion = '2.12'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
|
|
||||||
|
coilVersion = '3.0.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@ -207,7 +209,7 @@ dependencies {
|
|||||||
// 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'
|
||||||
// WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead
|
// WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:176da72cb4c3ec4679211339b0e59f6b01bf2f52'
|
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 **/
|
||||||
@ -272,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}"
|
||||||
|
@ -1,279 +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.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;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import coil.ImageLoader;
|
|
||||||
import coil.ImageLoaderFactory;
|
|
||||||
import coil.util.DebugLogger;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@HiltAndroidApp
|
|
||||||
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()));
|
|
||||||
|
|
||||||
BridgeStateSaverInitializer.init(this);
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
@ -166,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);
|
||||||
}
|
}
|
||||||
@ -176,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)
|
||||||
|
@ -6,9 +6,11 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
|
|
||||||
public class EmptyFragment extends BaseFragment {
|
public class EmptyFragment extends BaseFragment {
|
||||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||||
@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment {
|
|||||||
final Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
|
||||||
showMessage ? View.VISIBLE : View.GONE);
|
final ComposeView composeView = view.findViewById(R.id.empty_state_view);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(composeView);
|
||||||
|
composeView.setVisibility(showMessage ? View.VISIBLE : View.GONE);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +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 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;
|
||||||
|
@ -10,7 +10,6 @@ import android.graphics.Color;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@ -45,6 +44,8 @@ import org.schabi.newpipe.fragments.detail.TabAdapter;
|
|||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@ -60,7 +61,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 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;
|
||||||
@ -199,6 +200,11 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
binding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getContentNotSupported()
|
||||||
|
);
|
||||||
|
|
||||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
binding.viewPager.setAdapter(tabAdapter);
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
@ -645,8 +651,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||||
binding.channelKaomoji.setText("(︶︹︺)");
|
|
||||||
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
@ -79,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
@ -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;
|
||||||
|
@ -64,6 +64,8 @@ import org.schabi.newpipe.ktx.AnimationType;
|
|||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@ -344,6 +346,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
searchBinding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getNoSearchResult());
|
||||||
|
|
||||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||||
// animations are just strange and useless, since the suggestions keep changing too much
|
// animations are just strange and useless, since the suggestions keep changing too much
|
||||||
searchBinding.suggestionsList.setItemAnimator(null);
|
searchBinding.suggestionsList.setItemAnimator(null);
|
||||||
|
@ -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",
|
||||||
|
@ -38,6 +38,8 @@ 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.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
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.DebounceSavable;
|
||||||
@ -123,6 +125,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
itemListAdapter.setUseItemHandle(true);
|
itemListAdapter.setUseItemHandle(true);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
rootView.findViewById(R.id.empty_state_view),
|
||||||
|
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -74,6 +74,7 @@ import org.schabi.newpipe.ktx.slideUp
|
|||||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
import org.schabi.newpipe.util.DeviceUtils
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
@ -132,6 +133,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||||
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
||||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||||
|
feedBinding.emptyStateView.setEmptyStateComposable()
|
||||||
super.onViewCreated(rootView, savedInstanceState)
|
super.onViewCreated(rootView, savedInstanceState)
|
||||||
|
|
||||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||||
|
@ -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),
|
||||||
|
@ -56,6 +56,7 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
|||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
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.ServiceHelper
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
@ -257,6 +258,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
binding.itemsList.adapter = groupAdapter
|
binding.itemsList.adapter = groupAdapter
|
||||||
binding.itemsList.itemAnimator = null
|
binding.itemsList.itemAnimator = null
|
||||||
|
|
||||||
|
binding.emptyStateView.setEmptyStateComposable()
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
|
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
|
||||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
||||||
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
|
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
|
||||||
|
@ -3,14 +3,18 @@ package org.schabi.newpipe.local.subscription.item
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import com.xwray.groupie.viewbinding.BindableItem
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.ListEmptyViewBinding
|
import org.schabi.newpipe.databinding.ListEmptyViewSubscriptionsBinding
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When there are no subscriptions, show a hint to the user about how to import subscriptions
|
* When there are no subscriptions, show a hint to the user about how to import subscriptions
|
||||||
*/
|
*/
|
||||||
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
|
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewSubscriptionsBinding>() {
|
||||||
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions
|
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions
|
||||||
override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {}
|
override fun bind(viewBinding: ListEmptyViewSubscriptionsBinding, position: Int) {
|
||||||
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
|
viewBinding.root.setEmptyStateComposable(EmptyStateSpec.NoSubscriptionsHint)
|
||||||
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
|
}
|
||||||
|
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
|
||||||
|
override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@ -19,6 +20,8 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.image.CoilHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ public class SelectChannelFragment extends DialogFragment {
|
|||||||
private OnCancelListener onCancelListener = null;
|
private OnCancelListener onCancelListener = null;
|
||||||
|
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
private TextView emptyView;
|
private ComposeView emptyView;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
|
|
||||||
private List<SubscriptionEntity> subscriptions = new Vector<>();
|
private List<SubscriptionEntity> subscriptions = new Vector<>();
|
||||||
@ -91,6 +94,9 @@ public class SelectChannelFragment extends DialogFragment {
|
|||||||
|
|
||||||
progressBar = v.findViewById(R.id.progressBar);
|
progressBar = v.findViewById(R.id.progressBar);
|
||||||
emptyView = v.findViewById(R.id.empty_state_view);
|
emptyView = v.findViewById(R.id.empty_state_view);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(emptyView,
|
||||||
|
EmptyStateSpec.Companion.getNoSubscriptions());
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
recyclerView.setVisibility(View.GONE);
|
recyclerView.setVisibility(View.GONE);
|
||||||
emptyView.setVisibility(View.GONE);
|
emptyView.setVisibility(View.GONE);
|
||||||
|
@ -11,6 +11,7 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@ -27,6 +28,8 @@ import org.schabi.newpipe.error.ErrorUtil;
|
|||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
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.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.image.CoilHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -40,7 +43,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
private OnSelectedListener onSelectedListener = null;
|
private OnSelectedListener onSelectedListener = null;
|
||||||
|
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
private TextView emptyView;
|
private ComposeView emptyView;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private Disposable disposable = null;
|
private Disposable disposable = null;
|
||||||
|
|
||||||
@ -62,6 +65,8 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
recyclerView = v.findViewById(R.id.items_list);
|
recyclerView = v.findViewById(R.id.items_list);
|
||||||
emptyView = v.findViewById(R.id.empty_state_view);
|
emptyView = v.findViewById(R.id.empty_state_view);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(emptyView,
|
||||||
|
EmptyStateSpec.Companion.getNoBookmarkedPlaylist());
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
|
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
|
||||||
recyclerView.setAdapter(playlistAdapter);
|
recyclerView.setAdapter(playlistAdapter);
|
||||||
|
@ -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) {
|
||||||
|
@ -11,6 +11,8 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
|
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -39,6 +41,9 @@ public class PreferenceSearchFragment extends Fragment {
|
|||||||
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
|
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
binding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getNoSearchMaxSizeResult());
|
||||||
|
|
||||||
adapter = new PreferenceSearchAdapter();
|
adapter = new PreferenceSearchAdapter();
|
||||||
adapter.setOnItemClickListener(this::onItemClicked);
|
adapter.setOnItemClickListener(this::onItemClicked);
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
package org.schabi.newpipe.ui.components.common
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
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.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NoItemsMessage(@StringRes message: Int) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentSize(Alignment.Center),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(text = "(╯°-°)╯", fontSize = 35.sp)
|
|
||||||
Text(text = stringResource(id = message), fontSize = 24.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
|
||||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
private fun NoItemsMessagePreview() {
|
|
||||||
AppTheme {
|
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
|
||||||
NoItemsMessage(message = R.string.no_videos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ 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
|
||||||
|
@ -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
|
||||||
|
@ -26,9 +26,10 @@ import org.schabi.newpipe.R
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode
|
import org.schabi.newpipe.info_list.ItemViewMode
|
||||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
|
||||||
import org.schabi.newpipe.ui.components.items.ItemList
|
import org.schabi.newpipe.ui.components.items.ItemList
|
||||||
import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem
|
import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||||
|
|
||||||
@ -41,9 +42,6 @@ fun RelatedItems(info: StreamInfo) {
|
|||||||
mutableStateOf(sharedPreferences.getBoolean(key, false))
|
mutableStateOf(sharedPreferences.getBoolean(key, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.relatedItems.isEmpty()) {
|
|
||||||
NoItemsMessage(message = R.string.no_videos)
|
|
||||||
} else {
|
|
||||||
ItemList(
|
ItemList(
|
||||||
items = info.relatedItems,
|
items = info.relatedItems,
|
||||||
mode = ItemViewMode.LIST,
|
mode = ItemViewMode.LIST,
|
||||||
@ -75,10 +73,14 @@ fun RelatedItems(info: StreamInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (info.relatedItems.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateComposable(EmptyStateSpec.NoVideos)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
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.Page
|
import org.schabi.newpipe.extractor.Page
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -38,7 +39,8 @@ import org.schabi.newpipe.extractor.stream.Description
|
|||||||
import org.schabi.newpipe.paging.CommentRepliesSource
|
import org.schabi.newpipe.paging.CommentRepliesSource
|
||||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -130,13 +132,17 @@ private fun CommentRepliesDialog(
|
|||||||
val refresh = comments.loadState.refresh
|
val refresh = comments.loadState.refresh
|
||||||
if (refresh is LoadState.Loading) {
|
if (refresh is LoadState.Loading) {
|
||||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||||
} else {
|
} else if (refresh is LoadState.Error) {
|
||||||
val message = if (refresh is LoadState.Error) {
|
// TODO use error panel instead
|
||||||
R.string.error_unable_to_load_comments
|
EmptyStateComposable(
|
||||||
} else {
|
EmptyStateSpec.DisabledComments.copy(
|
||||||
R.string.no_comments
|
descriptionText = {
|
||||||
|
stringResource(R.string.error_unable_to_load_comments)
|
||||||
}
|
}
|
||||||
NoItemsMessage(message)
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyStateComposable(EmptyStateSpec.NoComments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -28,7 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||||
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.comments.CommentsInfoItem
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
import org.schabi.newpipe.extractor.stream.Description
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@ -28,7 +29,8 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
|||||||
import org.schabi.newpipe.extractor.stream.Description
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
||||||
import org.schabi.newpipe.viewmodels.util.Resource
|
import org.schabi.newpipe.viewmodels.util.Resource
|
||||||
@ -66,11 +68,11 @@ private fun CommentSection(
|
|||||||
|
|
||||||
if (commentInfo.isCommentsDisabled) {
|
if (commentInfo.isCommentsDisabled) {
|
||||||
item {
|
item {
|
||||||
NoItemsMessage(R.string.comments_are_disabled)
|
EmptyStateComposable(EmptyStateSpec.DisabledComments)
|
||||||
}
|
}
|
||||||
} else if (count == 0) {
|
} else if (count == 0) {
|
||||||
item {
|
item {
|
||||||
NoItemsMessage(R.string.no_comments)
|
EmptyStateComposable(EmptyStateSpec.NoComments)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// do not show anything if the comment count is unknown
|
// do not show anything if the comment count is unknown
|
||||||
@ -95,7 +97,14 @@ private fun CommentSection(
|
|||||||
|
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
item {
|
item {
|
||||||
NoItemsMessage(R.string.error_unable_to_load_comments)
|
// TODO use error panel instead
|
||||||
|
EmptyStateComposable(
|
||||||
|
EmptyStateSpec.DisabledComments.copy(
|
||||||
|
descriptionText = {
|
||||||
|
stringResource(R.string.error_unable_to_load_comments)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +119,14 @@ private fun CommentSection(
|
|||||||
|
|
||||||
is Resource.Error -> {
|
is Resource.Error -> {
|
||||||
item {
|
item {
|
||||||
NoItemsMessage(R.string.error_unable_to_load_comments)
|
// TODO use error panel instead
|
||||||
|
EmptyStateComposable(
|
||||||
|
EmptyStateSpec.DisabledComments.copy(
|
||||||
|
descriptionText = {
|
||||||
|
stringResource(R.string.error_unable_to_load_comments)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package org.schabi.newpipe.ui.emptystate
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyStateComposable(
|
||||||
|
spec: EmptyStateSpec,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) = EmptyStateComposable(
|
||||||
|
modifier = spec.modifier(modifier),
|
||||||
|
emojiText = spec.emojiText(),
|
||||||
|
descriptionText = spec.descriptionText(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyStateComposable(
|
||||||
|
emojiText: String,
|
||||||
|
descriptionText: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = emojiText,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 6.dp)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
text = descriptionText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, backgroundColor = Color.WHITE.toLong())
|
||||||
|
@Composable
|
||||||
|
fun EmptyStateComposableGenericErrorPreview() {
|
||||||
|
AppTheme {
|
||||||
|
EmptyStateComposable(EmptyStateSpec.GenericError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, backgroundColor = Color.WHITE.toLong())
|
||||||
|
@Composable
|
||||||
|
fun EmptyStateComposableNoCommentPreview() {
|
||||||
|
AppTheme {
|
||||||
|
EmptyStateComposable(EmptyStateSpec.NoComments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EmptyStateSpec(
|
||||||
|
val modifier: (Modifier) -> Modifier,
|
||||||
|
val emojiText: @Composable () -> String,
|
||||||
|
val descriptionText: @Composable () -> String,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val GenericError =
|
||||||
|
EmptyStateSpec(
|
||||||
|
modifier = {
|
||||||
|
it
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 128.dp)
|
||||||
|
},
|
||||||
|
emojiText = { "¯\\_(ツ)_/¯" },
|
||||||
|
descriptionText = { stringResource(id = R.string.empty_list_subtitle) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoVideos =
|
||||||
|
EmptyStateSpec(
|
||||||
|
modifier = {
|
||||||
|
it
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 128.dp)
|
||||||
|
},
|
||||||
|
emojiText = { "(╯°-°)╯" },
|
||||||
|
descriptionText = { stringResource(id = R.string.no_videos) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoComments =
|
||||||
|
EmptyStateSpec(
|
||||||
|
modifier = {
|
||||||
|
it
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 128.dp)
|
||||||
|
},
|
||||||
|
emojiText = { "¯\\_(╹x╹)_/¯" },
|
||||||
|
descriptionText = { stringResource(id = R.string.no_comments) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val DisabledComments =
|
||||||
|
NoComments.copy(
|
||||||
|
descriptionText = { stringResource(id = R.string.comments_are_disabled) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoSearchResult =
|
||||||
|
NoComments.copy(
|
||||||
|
modifier = { it },
|
||||||
|
emojiText = { "╰(°●°╰)" },
|
||||||
|
descriptionText = { stringResource(id = R.string.search_no_results) }
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoSearchMaxSizeResult =
|
||||||
|
NoSearchResult.copy(
|
||||||
|
modifier = { it.fillMaxSize() },
|
||||||
|
)
|
||||||
|
|
||||||
|
val ContentNotSupported =
|
||||||
|
NoComments.copy(
|
||||||
|
modifier = { it.padding(top = 90.dp) },
|
||||||
|
emojiText = { "(︶︹︺)" },
|
||||||
|
descriptionText = { stringResource(id = R.string.content_not_supported) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoBookmarkedPlaylist =
|
||||||
|
EmptyStateSpec(
|
||||||
|
modifier = { it },
|
||||||
|
emojiText = { "(╥﹏╥)" },
|
||||||
|
descriptionText = { stringResource(id = R.string.no_playlist_bookmarked_yet) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoSubscriptionsHint =
|
||||||
|
EmptyStateSpec(
|
||||||
|
modifier = { it },
|
||||||
|
emojiText = { "(꩜ᯅ꩜)" },
|
||||||
|
descriptionText = { stringResource(id = R.string.import_subscriptions_hint) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val NoSubscriptions =
|
||||||
|
NoSubscriptionsHint.copy(
|
||||||
|
descriptionText = { stringResource(id = R.string.no_channel_subscribed_yet) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
@file:JvmName("EmptyStateUtil")
|
||||||
|
|
||||||
|
package org.schabi.newpipe.ui.emptystate
|
||||||
|
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun ComposeView.setEmptyStateComposable(
|
||||||
|
spec: EmptyStateSpec = EmptyStateSpec.GenericError,
|
||||||
|
strategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
|
||||||
|
) = apply {
|
||||||
|
setViewCompositionStrategy(strategy)
|
||||||
|
setContent {
|
||||||
|
AppTheme {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
EmptyStateComposable(
|
||||||
|
spec = spec
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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,43 +30,66 @@ 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 =
|
||||||
|
getImageRequest(context, url, R.drawable.placeholder_thumbnail_video)
|
||||||
.target(target)
|
.target(target)
|
||||||
.transformations(object : Transformation {
|
.transformations(
|
||||||
|
object : Transformation() {
|
||||||
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
|
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
|
||||||
|
|
||||||
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
override suspend fun transform(
|
||||||
|
input: Bitmap,
|
||||||
|
size: Size,
|
||||||
|
): Bitmap {
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - transform() called")
|
Log.d(TAG, "Thumbnail - transform() called")
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationThumbnailWidth = min(
|
val notificationThumbnailWidth =
|
||||||
|
min(
|
||||||
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
|
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
|
||||||
input.width.toFloat()
|
input.width.toFloat(),
|
||||||
).toInt()
|
).toInt()
|
||||||
|
|
||||||
var newHeight = input.height / (input.width / notificationThumbnailWidth)
|
var newHeight = input.height / (input.width / notificationThumbnailWidth)
|
||||||
@ -77,33 +104,45 @@ object CoilHelper {
|
|||||||
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,9 +151,10 @@ 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 =
|
||||||
|
getImageRequest(target.context, url, placeholderResId, showPlaceholder)
|
||||||
.target(target)
|
.target(target)
|
||||||
.build()
|
.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)
|
||||||
|
@ -22,6 +22,7 @@ import androidx.activity.result.ActivityResultLauncher;
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
@ -34,6 +35,7 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
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.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -108,7 +110,8 @@ public class MissionsFragment extends Fragment {
|
|||||||
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
mEmpty = v.findViewById(R.id.list_empty_view);
|
mEmpty = v.findViewById(R.id.empty_state_view);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable((ComposeView) mEmpty);
|
||||||
mList = v.findViewById(R.id.mission_recycler);
|
mList = v.findViewById(R.id.mission_recycler);
|
||||||
|
|
||||||
// Init layouts managers
|
// Init layouts managers
|
||||||
|
@ -24,15 +24,15 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<include
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
layout="@layout/list_empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_marginTop="50dp"
|
android:layout_marginTop="50dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible"
|
||||||
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -168,37 +168,14 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="90dp"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible"
|
||||||
|
/>
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/channel_kaomoji"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/error_content_not_supported"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="20dp"
|
|
||||||
android:text="@string/content_not_supported"
|
|
||||||
android:textSize="15sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!--ERROR PANEL-->
|
<!--ERROR PANEL-->
|
||||||
<include
|
<include
|
||||||
|
@ -20,15 +20,15 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<include
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
layout="@layout/list_empty_view"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible"
|
||||||
|
/>
|
||||||
|
|
||||||
<!--ERROR PANEL-->
|
<!--ERROR PANEL-->
|
||||||
<include
|
<include
|
||||||
|
@ -20,36 +20,14 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="90dp"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible"
|
||||||
|
/>
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/channel_kaomoji"
|
|
||||||
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/channel_no_videos"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/empty_view_no_videos"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!--ERROR PANEL-->
|
<!--ERROR PANEL-->
|
||||||
<include
|
<include
|
||||||
|
@ -7,12 +7,13 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<include
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
layout="@layout/list_empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginTop="90dp" />
|
android:layout_marginTop="90dp"
|
||||||
|
/>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
@ -140,15 +140,15 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<include
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
layout="@layout/list_empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_marginTop="50dp"
|
android:layout_marginTop="50dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible"
|
||||||
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -52,33 +52,14 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
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:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/search_no_results"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/suggestions_panel"
|
android:id="@+id/suggestions_panel"
|
||||||
|
@ -24,16 +24,16 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<include
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
layout="@layout/list_empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/items_list"
|
android:layout_below="@id/items_list"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_marginTop="50dp"
|
android:layout_marginTop="50dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible"
|
||||||
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minHeight="128dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="¯\\_(ツ)_/¯"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
tools:ignore="HardcodedText" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingHorizontal="16dp"
|
|
||||||
android:text="@string/empty_list_subtitle" />
|
|
||||||
</LinearLayout>
|
|
@ -1,25 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.compose.ui.platform.ComposeView
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
|
||||||
android:minHeight="128dp"
|
android:minHeight="128dp"
|
||||||
android:orientation="vertical">
|
xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="¯\\_(ツ)_/¯"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
tools:ignore="HardcodedText" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingHorizontal="16dp"
|
|
||||||
android:text="@string/import_subscriptions_hint" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<include
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/list_empty_view"
|
android:id="@+id/empty_state_view"
|
||||||
layout="@layout/list_empty_view"
|
android:layout_width="match_parent"
|
||||||
android:visibility="gone" />
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="128dp" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/mission_recycler"
|
android:id="@+id/mission_recycler"
|
||||||
|
@ -24,14 +24,11 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:listitem="@layout/select_channel_item" />
|
tools:listitem="@layout/select_channel_item" />
|
||||||
|
|
||||||
|
<androidx.compose.ui.platform.ComposeView
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp" />
|
||||||
android:text="@string/no_channel_subscribed_yet"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceListItem" />
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
|
@ -26,14 +26,11 @@
|
|||||||
|
|
||||||
</androidx.recyclerview.widget.RecyclerView>
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
|
||||||
|
<androidx.compose.ui.platform.ComposeView
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp" />
|
||||||
android:text="@string/no_playlist_bookmarked_yet"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceListItem" />
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
|
@ -12,33 +12,14 @@
|
|||||||
android:layout_height="4dp"
|
android:layout_height="4dp"
|
||||||
android:background="?attr/toolbar_shadow" />
|
android:background="?attr/toolbar_shadow" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/empty_state_view"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="gone">
|
tools:visibility="gone"
|
||||||
|
/>
|
||||||
<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:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/search_no_results"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/searchResults"
|
android:id="@+id/searchResults"
|
||||||
|
Loading…
Reference in New Issue
Block a user