1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-11-25 11:39:32 +01:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Profpatsch
7df62000a9
Merge 3c0b0be297 into 13585ca0be 2024-11-20 15:09:58 +01:00
Profpatsch
3c0b0be297 build.gradle: fix NewPipeExtractor hash
If we use the whole hash, jitpack returns a 404 for some reason (maybe
because the URL gets too long).

This should fix that.
2024-11-20 15:09:54 +01:00
toliuweijing
e05eef09c6 boost error hint color 2024-11-20 11:50:18 +01:00
toliuweijing
ec45f32b95 Migrate empty_state_view to Jetpack Compose 2024-11-20 11:50:16 +01:00
40 changed files with 692 additions and 601 deletions

View File

@ -124,8 +124,6 @@ ext {
leakCanaryVersion = '2.12'
stethoVersion = '1.6.0'
coilVersion = '3.0.3'
}
configurations {
@ -209,7 +207,7 @@ dependencies {
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
// WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead
implementation 'com.github.TeamNewPipe:NewPipeExtractor:d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:176da72cb4c3ec'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/
@ -274,8 +272,7 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
// Image loading
implementation "io.coil-kt.coil3:coil-compose:${coilVersion}"
implementation "io.coil-kt.coil3:coil-network-okhttp:${coilVersion}"
implementation 'io.coil-kt:coil-compose:2.7.0'
// Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}"

View File

@ -0,0 +1,279 @@
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;
}
}

View File

@ -1,283 +0,0 @@
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
}
}

View File

@ -166,7 +166,7 @@ public class MainActivity extends AppCompatActivity {
NotificationWorker.initialize(this);
}
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
&& !App.getInstance().isFirstRun()
&& !App.getApp().isFirstRun()
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
@ -176,7 +176,7 @@ public class MainActivity extends AppCompatActivity {
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
final App app = App.getInstance();
final App app = App.getApp();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)

View File

@ -127,7 +127,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import coil3.util.CoilUtils;
import coil.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;

View File

@ -19,6 +19,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.compose.runtime.MutableState;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.MenuProvider;
@ -45,6 +46,7 @@ import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpecBuilder;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants;
@ -61,7 +63,7 @@ import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import coil3.util.CoilUtils;
import coil.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -103,6 +105,8 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
private SubscriptionEntity channelSubscription;
private MenuProvider menuProvider;
private MutableState<EmptyStateSpec> emptyStateSpec;
public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) {
final ChannelFragment instance = new ChannelFragment();
@ -200,10 +204,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
EmptyStateUtil.setEmptyStateComposable(
binding.emptyStateView,
EmptyStateSpec.Companion.getContentNotSupported()
);
emptyStateSpec = EmptyStateUtil.mutableStateOf(
EmptyStateSpec.Companion.getContentNotSupported());
EmptyStateUtil.setEmptyStateComposable(binding.emptyStateView, emptyStateSpec);
tabAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(tabAdapter);
@ -651,6 +654,10 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return;
}
binding.emptyStateView.setVisibility(View.VISIBLE);
emptyStateSpec.setValue(
new EmptyStateSpecBuilder(emptyStateSpec.getValue())
.descriptionVisibility(true)
.build()
);
}
}

View File

@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import coil3.util.CoilUtils;
import coil.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;

View File

@ -346,7 +346,7 @@ public final class InfoItemDialog {
public static void reportErrorDuringInitialization(final Throwable throwable,
final InfoItem item) {
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
throwable,
UserAction.OPEN_INFO_ITEM_DIALOG,
"none",

View File

@ -38,7 +38,6 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
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.OnClickGesture;
@ -125,10 +124,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
super.initViews(rootView, savedInstanceState);
itemListAdapter.setUseItemHandle(true);
EmptyStateUtil.setEmptyStateComposable(
rootView.findViewById(R.id.empty_state_view),
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
);
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
}
@Override

View File

@ -165,7 +165,7 @@ class FeedViewModel(
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer {
FeedViewModel(
App.instance,
App.getApp(),
groupId,
// Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext),

View File

@ -3,18 +3,14 @@ package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ListEmptyViewSubscriptionsBinding
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
import org.schabi.newpipe.databinding.ListEmptyViewBinding
/**
* When there are no subscriptions, show a hint to the user about how to import subscriptions
*/
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewSubscriptionsBinding>() {
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions
override fun bind(viewBinding: ListEmptyViewSubscriptionsBinding, position: Int) {
viewBinding.root.setEmptyStateComposable(EmptyStateSpec.NoSubscriptionsHint)
}
override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {}
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view)
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
}

View File

@ -46,7 +46,6 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static coil3.Image_androidKt.toBitmap;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -54,12 +53,14 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.util.Log;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableKt;
import androidx.core.math.MathUtils;
import androidx.preference.PreferenceManager;
@ -124,7 +125,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import coil3.target.Target;
import coil.target.Target;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -192,7 +193,7 @@ public final class Player implements PlaybackListener, Listener {
@Nullable
private Bitmap currentThumbnail;
@Nullable
private coil3.request.Disposable thumbnailDisposable;
private coil.request.Disposable thumbnailDisposable;
/*//////////////////////////////////////////////////////////////////////////
// Player
@ -788,26 +789,27 @@ public final class Player implements PlaybackListener, Listener {
// scale down the notification thumbnail for performance
final var thumbnailTarget = new Target() {
@Override
public void onError(@Nullable final coil3.Image error) {
public void onError(@Nullable final Drawable error) {
Log.e(TAG, "Thumbnail - onError() called");
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(null);
}
@Override
public void onStart(@Nullable final coil3.Image placeholder) {
public void onStart(@Nullable final Drawable placeholder) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onStart() called");
}
}
@Override
public void onSuccess(@NonNull final coil3.Image result) {
public void onSuccess(@NonNull final Drawable result) {
if (DEBUG) {
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.
onThumbnailLoaded(toBitmap(result));
onThumbnailLoaded(DrawableKt.toBitmapOrNull(result, result.getIntrinsicWidth(),
result.getIntrinsicHeight(), null));
}
};
thumbnailDisposable = CoilHelper.INSTANCE

View File

@ -16,8 +16,8 @@ import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
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
// context to bind/unbind a service is crucial
private Context getCommonContext() {
return App.getInstance();
return App.getApp();
}
public void startService(final boolean playAfterConnect,

View File

@ -179,7 +179,7 @@ public class SeekbarPreviewThumbnailHolder {
// 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
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url);
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getApp(), url);
if (sw != null) {
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "

View File

@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import coil3.SingletonImageLoader;
import coil.Coil;
public class ContentSettingsFragment extends BasePreferenceFragment {
private String youtubeRestrictedModeEnabledKey;
@ -41,7 +41,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
(preference, newValue) -> {
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
.fromPreferenceKey(requireContext(), (String) newValue));
final var loader = SingletonImageLoader.get(preference.getContext());
final var loader = Coil.imageLoader(preference.getContext());
loader.getMemoryCache().clear();
loader.getDiskCache().clear();
Toast.makeText(preference.getContext(),

View File

@ -156,7 +156,7 @@ public final class NewPipeSettings {
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
&& !prefs.getBoolean(disabledTunnelingKey, false);
if (App.getInstance().isFirstRun()
if (App.getApp().isFirstRun()
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
setMediaTunneling(context);
}

View File

@ -11,7 +11,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.compose.ui.platform.ComposeView;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -20,8 +19,6 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
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.image.CoilHelper;
@ -60,7 +57,7 @@ public class SelectChannelFragment extends DialogFragment {
private OnCancelListener onCancelListener = null;
private ProgressBar progressBar;
private ComposeView emptyView;
private TextView emptyView;
private RecyclerView recyclerView;
private List<SubscriptionEntity> subscriptions = new Vector<>();
@ -94,9 +91,6 @@ public class SelectChannelFragment extends DialogFragment {
progressBar = v.findViewById(R.id.progressBar);
emptyView = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView,
EmptyStateSpec.Companion.getNoSubscriptions());
progressBar.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);

View File

@ -28,7 +28,6 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
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;
@ -65,8 +64,7 @@ public class SelectPlaylistFragment extends DialogFragment {
recyclerView = v.findViewById(R.id.items_list);
emptyView = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView,
EmptyStateSpec.Companion.getNoBookmarkedPlaylist());
EmptyStateUtil.setEmptyStateText(emptyView, R.string.no_playlist_bookmarked_yet);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
recyclerView.setAdapter(playlistAdapter);

View File

@ -1,7 +1,5 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
@ -20,6 +18,8 @@ import java.util.Collections;
import java.util.HashSet;
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 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);
// no migration to run, already up to date
if (App.getInstance().isFirstRun()) {
if (App.getApp().isFirstRun()) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {

View File

@ -0,0 +1,42 @@
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)
}
}
}

View File

@ -18,7 +18,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil.compose.AsyncImage
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
import org.schabi.newpipe.util.Localization

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import coil.compose.AsyncImage
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.Localization

View File

@ -26,10 +26,9 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamType
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.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.util.NO_SERVICE_ID
@ -42,44 +41,43 @@ fun RelatedItems(info: StreamInfo) {
mutableStateOf(sharedPreferences.getBoolean(key, false))
}
ItemList(
items = info.relatedItems,
mode = ItemViewMode.LIST,
listHeader = {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = stringResource(R.string.auto_queue_description))
if (info.relatedItems.isEmpty()) {
NoItemsMessage(message = R.string.no_videos)
} else {
ItemList(
items = info.relatedItems,
mode = ItemViewMode.LIST,
listHeader = {
item {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = stringResource(R.string.auto_queue_toggle))
Switch(
checked = isAutoQueueEnabled,
onCheckedChange = {
isAutoQueueEnabled = it
sharedPreferences.edit {
putBoolean(key, it)
Text(text = stringResource(R.string.auto_queue_description))
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.auto_queue_toggle))
Switch(
checked = isAutoQueueEnabled,
onCheckedChange = {
isAutoQueueEnabled = it
sharedPreferences.edit {
putBoolean(key, it)
}
}
}
)
)
}
}
}
}
if (info.relatedItems.isEmpty()) {
item {
EmptyStateComposable(EmptyStateSpec.NoVideos)
}
}
}
)
)
}
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)

View File

@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil.compose.AsyncImage
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfoItem

View File

@ -20,7 +20,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
@ -39,8 +38,7 @@ import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.paging.CommentRepliesSource
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
import org.schabi.newpipe.ui.components.common.LoadingIndicator
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
import org.schabi.newpipe.ui.components.common.NoItemsMessage
import org.schabi.newpipe.ui.theme.AppTheme
@Composable
@ -132,17 +130,13 @@ private fun CommentRepliesDialog(
val refresh = comments.loadState.refresh
if (refresh is LoadState.Loading) {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
} else if (refresh is LoadState.Error) {
// TODO use error panel instead
EmptyStateComposable(
EmptyStateSpec.DisabledComments.copy(
descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
}
)
)
} else {
EmptyStateComposable(EmptyStateSpec.NoComments)
val message = if (refresh is LoadState.Error) {
R.string.error_unable_to_load_comments
} else {
R.string.no_comments
}
NoItemsMessage(message)
}
}
} else {

View File

@ -28,7 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil.compose.AsyncImage
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description

View File

@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -29,8 +28,7 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
import org.schabi.newpipe.ui.components.common.LoadingIndicator
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
import org.schabi.newpipe.ui.components.common.NoItemsMessage
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.viewmodels.CommentsViewModel
import org.schabi.newpipe.viewmodels.util.Resource
@ -68,11 +66,11 @@ private fun CommentSection(
if (commentInfo.isCommentsDisabled) {
item {
EmptyStateComposable(EmptyStateSpec.DisabledComments)
NoItemsMessage(R.string.comments_are_disabled)
}
} else if (count == 0) {
item {
EmptyStateComposable(EmptyStateSpec.NoComments)
NoItemsMessage(R.string.no_comments)
}
} else {
// do not show anything if the comment count is unknown
@ -97,14 +95,7 @@ private fun CommentSection(
is LoadState.Error -> {
item {
// TODO use error panel instead
EmptyStateComposable(
EmptyStateSpec.DisabledComments.copy(
descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
}
)
)
NoItemsMessage(R.string.error_unable_to_load_comments)
}
}
@ -119,14 +110,7 @@ private fun CommentSection(
is Resource.Error -> {
item {
// TODO use error panel instead
EmptyStateComposable(
EmptyStateSpec.DisabledComments.copy(
descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
}
)
)
NoItemsMessage(R.string.error_unable_to_load_comments)
}
}
}

View File

@ -7,17 +7,23 @@ 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.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.schabi.newpipe.R
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.ui.theme.errorHint
@Composable
fun EmptyStateComposable(
@ -25,35 +31,48 @@ fun EmptyStateComposable(
modifier: Modifier = Modifier,
) = EmptyStateComposable(
modifier = spec.modifier(modifier),
emojiModifier = spec.emojiModifier(),
emojiText = spec.emojiText(),
emojiTextStyle = spec.emojiTextStyle(),
descriptionModifier = spec.descriptionModifier(),
descriptionText = spec.descriptionText(),
descriptionTextStyle = spec.descriptionTextStyle(),
descriptionTextVisibility = spec.descriptionVisibility(),
)
@Composable
private fun EmptyStateComposable(
modifier: Modifier,
emojiModifier: Modifier,
emojiText: String,
emojiTextStyle: TextStyle,
descriptionModifier: Modifier,
descriptionText: String,
modifier: Modifier = Modifier,
descriptionTextStyle: TextStyle,
descriptionTextVisibility: Boolean,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.errorHint
) {
Text(
text = emojiText,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
)
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = emojiModifier,
text = emojiText,
style = emojiTextStyle,
)
Text(
modifier = Modifier
.padding(top = 6.dp)
.padding(horizontal = 16.dp),
text = descriptionText,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
if (descriptionTextVisibility) {
Text(
modifier = descriptionModifier,
text = descriptionText,
style = descriptionTextStyle,
)
}
}
}
}
@ -69,15 +88,21 @@ fun EmptyStateComposableGenericErrorPreview() {
@Composable
fun EmptyStateComposableNoCommentPreview() {
AppTheme {
EmptyStateComposable(EmptyStateSpec.NoComments)
EmptyStateComposable(EmptyStateSpec.NoComment)
}
}
data class EmptyStateSpec(
val modifier: (Modifier) -> Modifier,
val emojiModifier: () -> Modifier,
val emojiText: @Composable () -> String,
val emojiTextStyle: @Composable () -> TextStyle,
val descriptionText: @Composable () -> String,
val descriptionModifier: () -> Modifier,
val descriptionTextStyle: @Composable () -> TextStyle,
val descriptionVisibility: () -> Boolean = { true },
) {
companion object {
val GenericError =
@ -87,39 +112,38 @@ data class EmptyStateSpec(
.fillMaxWidth()
.heightIn(min = 128.dp)
},
emojiModifier = { Modifier },
emojiText = { "¯\\_(ツ)_/¯" },
emojiTextStyle = { MaterialTheme.typography.titleLarge },
descriptionModifier = {
Modifier
.padding(top = 6.dp)
.padding(horizontal = 16.dp)
},
descriptionText = { stringResource(id = R.string.empty_list_subtitle) },
descriptionTextStyle = { MaterialTheme.typography.bodyMedium }
)
val NoVideos =
val NoComment =
EmptyStateSpec(
modifier = {
it
.fillMaxWidth()
.heightIn(min = 128.dp)
},
modifier = { it.padding(top = 85.dp) },
emojiModifier = { Modifier.padding(bottom = 10.dp) },
emojiText = { "(╯°-°)╯" },
descriptionText = { stringResource(id = R.string.no_videos) },
)
val NoComments =
EmptyStateSpec(
modifier = {
it
.fillMaxWidth()
.heightIn(min = 128.dp)
emojiTextStyle = {
LocalTextStyle.current.merge(
fontFamily = FontFamily.Monospace,
fontSize = 35.sp,
)
},
emojiText = { "¯\\_(╹x╹)_/¯" },
descriptionModifier = { Modifier },
descriptionText = { stringResource(id = R.string.no_comments) },
)
val DisabledComments =
NoComments.copy(
descriptionText = { stringResource(id = R.string.comments_are_disabled) },
descriptionTextStyle = {
LocalTextStyle.current.merge(fontSize = 24.sp)
}
)
val NoSearchResult =
NoComments.copy(
NoComment.copy(
modifier = { it },
emojiText = { "╰(°●°╰)" },
descriptionText = { stringResource(id = R.string.search_no_results) }
@ -131,29 +155,14 @@ data class EmptyStateSpec(
)
val ContentNotSupported =
NoComments.copy(
NoComment.copy(
modifier = { it.padding(top = 90.dp) },
emojiText = { "(︶︹︺)" },
emojiTextStyle = { LocalTextStyle.current.merge(fontSize = 45.sp) },
descriptionModifier = { Modifier.padding(top = 20.dp) },
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) },
descriptionTextStyle = { LocalTextStyle.current.merge(fontSize = 15.sp) },
descriptionVisibility = { false },
)
}
}

View File

@ -2,13 +2,33 @@
package org.schabi.newpipe.ui.emptystate
import androidx.compose.material3.LocalContentColor
import androidx.annotation.StringRes
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.material3.Text
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.ui.theme.errorHint
import androidx.compose.runtime.mutableStateOf as composeRuntimeMutableStateOf
@JvmOverloads
fun ComposeView.setEmptyStateText(
@StringRes stringRes: Int,
strategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
) = apply {
setViewCompositionStrategy(strategy)
setContent {
AppTheme {
Text(
text = stringResource(id = stringRes),
color = MaterialTheme.colorScheme.errorHint,
)
}
}
}
@JvmOverloads
fun ComposeView.setEmptyStateComposable(
@ -18,13 +38,50 @@ fun ComposeView.setEmptyStateComposable(
setViewCompositionStrategy(strategy)
setContent {
AppTheme {
CompositionLocalProvider(
LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background)
) {
EmptyStateComposable(
spec = spec
)
}
EmptyStateComposable(
spec = spec
)
}
}
}
@JvmOverloads
fun ComposeView.setEmptyStateComposable(
spec: State<EmptyStateSpec>,
strategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
) = apply {
setViewCompositionStrategy(strategy)
setContent {
AppTheme {
EmptyStateComposable(
spec = spec.value,
)
}
}
}
/**
* Used in Java land to bridge the [MutableState] API.
*/
fun <T> mutableStateOf(param: T): MutableState<T> {
return composeRuntimeMutableStateOf(param)
}
/**
* Used in Java land to modify [EmptyStateSpec] properties.
* TODO: remove after Kotlin migration
*/
class EmptyStateSpecBuilder(var spec: EmptyStateSpec) {
fun descriptionText(@StringRes stringRes: Int) = apply {
spec = spec.copy(
descriptionText = { stringResource(id = stringRes) }
)
}
fun descriptionVisibility(descriptionTextVisibility: Boolean) = apply {
spec = spec.copy(descriptionVisibility = { descriptionTextVisibility })
}
fun build() = spec
}

View File

@ -0,0 +1,19 @@
package org.schabi.newpipe.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
* Extended color for error hint.
*/
val md_theme_light_error_hint = Color(0xCC000000)
val md_theme_dark_error_hint = Color(0xCCFFFFFF)
val ColorScheme.errorHint: Color
@Composable get() = if (isSystemInDarkTheme()) {
md_theme_dark_error_hint
} else {
md_theme_light_error_hint
}

View File

@ -130,7 +130,7 @@ public final class DeviceUtils {
}
isFireTV =
App.getInstance().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
return isFireTV;
}
@ -139,7 +139,7 @@ public final class DeviceUtils {
return isTV;
}
final PackageManager pm = App.getInstance().getPackageManager();
final PackageManager pm = App.getApp().getPackageManager();
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)

View File

@ -21,7 +21,7 @@ object ReleaseVersionUtil {
val certificates = mapOf(
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
)
val app = App.instance
val app = App.getApp()
try {
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
} catch (e: PackageManager.NameNotFoundException) {

View File

@ -1,7 +1,6 @@
package org.schabi.newpipe.util.external_communication;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static coil3.Image_androidKt.toBitmap;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
@ -32,9 +31,9 @@ import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import coil3.SingletonImageLoader;
import coil3.disk.DiskCache;
import coil3.memory.MemoryCache;
import coil.Coil;
import coil.disk.DiskCache;
import coil.memory.MemoryCache;
public final class ShareUtils {
private static final String TAG = ShareUtils.class.getSimpleName();
@ -378,13 +377,13 @@ public final class ShareUtils {
// 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
final Context applicationContext = context.getApplicationContext();
final var loader = SingletonImageLoader.get(context);
final var loader = Coil.imageLoader(context);
final var value = loader.getMemoryCache()
.get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap()));
final Bitmap cachedBitmap;
if (value != null) {
cachedBitmap = toBitmap(value.getImage());
cachedBitmap = value.getBitmap();
} else {
try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) {
if (snapshot != null) {

View File

@ -5,18 +5,14 @@ import android.graphics.Bitmap
import android.util.Log
import android.widget.ImageView
import androidx.annotation.DrawableRes
import coil3.executeBlocking
import coil3.imageLoader
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.request.error
import coil3.request.placeholder
import coil3.request.target
import coil3.request.transformations
import coil3.size.Size
import coil3.target.Target
import coil3.toBitmap
import coil3.transform.Transformation
import androidx.core.graphics.drawable.toBitmapOrNull
import coil.executeBlocking
import coil.imageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import coil.size.Size
import coil.target.Target
import coil.transform.Transformation
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Image
@ -30,119 +26,84 @@ object CoilHelper {
fun loadBitmapBlocking(
context: Context,
url: String?,
@DrawableRes placeholderResId: Int = 0,
): Bitmap? =
context.imageLoader
.executeBlocking(getImageRequest(context, url, placeholderResId).build())
.image
?.toBitmap()
@DrawableRes placeholderResId: Int = 0
): Bitmap? {
val request = getImageRequest(context, url, placeholderResId).build()
return context.imageLoader.executeBlocking(request).drawable?.toBitmapOrNull()
}
fun loadAvatar(
target: ImageView,
images: List<Image>,
) {
fun loadAvatar(target: ImageView, images: List<Image>) {
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)
}
fun loadThumbnail(
target: ImageView,
images: List<Image>,
) {
fun loadThumbnail(target: ImageView, images: List<Image>) {
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)
}
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 request =
getImageRequest(context, url, R.drawable.placeholder_thumbnail_video)
.target(target)
.transformations(
object : Transformation() {
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
val request = getImageRequest(context, url, R.drawable.placeholder_thumbnail_video)
.target(target)
.transformations(object : Transformation {
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
override suspend fun transform(
input: Bitmap,
size: Size,
): Bitmap {
if (MainActivity.DEBUG) {
Log.d(TAG, "Thumbnail - transform() called")
}
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
if (MainActivity.DEBUG) {
Log.d(TAG, "Thumbnail - transform() called")
}
val notificationThumbnailWidth =
min(
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
input.width.toFloat(),
).toInt()
val notificationThumbnailWidth = min(
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
input.width.toFloat()
).toInt()
var newHeight = input.height / (input.width / notificationThumbnailWidth)
val result = input.scale(notificationThumbnailWidth, newHeight)
var newHeight = input.height / (input.width / notificationThumbnailWidth)
val result = input.scale(notificationThumbnailWidth, newHeight)
return if (result == input || !result.isMutable) {
// create a new mutable bitmap to prevent strange crashes on some
// devices (see #4638)
newHeight = input.height / (input.width / (notificationThumbnailWidth - 1))
input.scale(notificationThumbnailWidth, newHeight)
} else {
result
}
}
},
).build()
return if (result == input || !result.isMutable) {
// create a new mutable bitmap to prevent strange crashes on some
// devices (see #4638)
newHeight = input.height / (input.width / (notificationThumbnailWidth - 1))
input.scale(notificationThumbnailWidth, newHeight)
} else {
result
}
}
})
.build()
return context.imageLoader.enqueue(request)
}
fun loadDetailsThumbnail(
target: ImageView,
images: List<Image>,
) {
fun loadDetailsThumbnail(target: ImageView, images: List<Image>) {
val url = ImageStrategy.choosePreferredImage(images)
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)
}
fun loadPlaylistThumbnail(
target: ImageView,
images: List<Image>,
) {
fun loadPlaylistThumbnail(target: ImageView, images: List<Image>) {
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)
}
private fun loadImageDefault(
target: ImageView,
images: List<Image>,
@DrawableRes placeholderResId: Int,
@DrawableRes placeholderResId: Int
) {
loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId)
}
@ -151,12 +112,11 @@ object CoilHelper {
target: ImageView,
url: String?,
@DrawableRes placeholderResId: Int,
showPlaceholder: Boolean = true,
showPlaceholder: Boolean = true
) {
val request =
getImageRequest(target.context, url, placeholderResId, showPlaceholder)
.target(target)
.build()
val request = getImageRequest(target.context, url, placeholderResId, showPlaceholder)
.target(target)
.build()
target.context.imageLoader.enqueue(request)
}
@ -164,15 +124,14 @@ object CoilHelper {
context: Context,
url: String?,
@DrawableRes placeholderResId: Int,
showPlaceholderWhileLoading: Boolean = true,
showPlaceholderWhileLoading: Boolean = true
): ImageRequest.Builder {
// 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
// for URLs stored in the database)
val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() }
return ImageRequest
.Builder(context)
return ImageRequest.Builder(context)
.data(takenUrl)
.error(placeholderResId)
.memoryCacheKey(takenUrl)

View File

@ -22,7 +22,6 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.compose.ui.platform.ComposeView;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
@ -35,7 +34,6 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
@ -110,8 +108,7 @@ public class MissionsFragment extends Fragment {
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
// Views
mEmpty = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable((ComposeView) mEmpty);
mEmpty = v.findViewById(R.id.list_empty_view);
mList = v.findViewById(R.id.mission_recycler);
// Init layouts managers

View File

@ -0,0 +1,25 @@
<?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>

View File

@ -1,6 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView
<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"
xmlns:android="http://schemas.android.com/apk/res/android" />
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/import_subscriptions_hint" />
</LinearLayout>

View File

@ -3,11 +3,10 @@
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="128dp" />
<include
android:id="@+id/list_empty_view"
layout="@layout/list_empty_view"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mission_recycler"

View File

@ -24,11 +24,14 @@
android:layout_height="wrap_content"
tools:listitem="@layout/select_channel_item" />
<androidx.compose.ui.platform.ComposeView
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/empty_state_view"
android:layout_width="match_parent"
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
android:id="@+id/progressBar"

View File

@ -30,7 +30,8 @@
android:id="@+id/empty_state_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
android:layout_margin="10dp"
/>
<ProgressBar
android:id="@+id/progressBar"