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

Compare commits

...

8 Commits

Author SHA1 Message Date
Isira Seneviratne
4e55f1bee6 Merge branch 'refactor' into About-Compose 2024-11-21 21:11:52 +05:30
Stypox
295f719b77
Merge pull request #11723 from Isira-Seneviratne/Coil-3
Migrate to Coil 3
2024-11-21 10:56:07 +01:00
Stypox
b584353f4d
Small fixes to code style 2024-11-21 10:52:15 +01:00
Isira Seneviratne
d73314b4dd Make App instance variable immutable outside class 2024-11-21 08:09:57 +05:30
Isira Seneviratne
9f4a33c7a8 Fix lint 2024-11-21 06:56:10 +05:30
Isira Seneviratne
3a9540b042
Update app/src/main/java/org/schabi/newpipe/App.kt
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-11-20 16:04:39 +05:30
Isira Seneviratne
ca855cbca0 Migrate to Coil 3 2024-11-20 09:28:20 +05:30
Isira Seneviratne
6a98b1dac7 Rename .java to .kt 2024-11-20 08:44:16 +05:30
23 changed files with 419 additions and 372 deletions

View File

@ -125,6 +125,8 @@ ext {
leakCanaryVersion = '2.12' leakCanaryVersion = '2.12'
stethoVersion = '1.6.0' stethoVersion = '1.6.0'
coilVersion = '3.0.3'
} }
configurations { configurations {
@ -208,7 +210,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 **/
@ -270,7 +272,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}"

View File

@ -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;
}
}

View 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
}
}

View File

@ -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)

View File

@ -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;

View File

@ -60,7 +60,7 @@ import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import coil.util.CoilUtils; import coil3.util.CoilUtils;
import 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;

View File

@ -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;

View File

@ -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",

View File

@ -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),

View File

@ -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

View File

@ -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,

View File

@ -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 "

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.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(),

View File

@ -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);
} }

View File

@ -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) {

View File

@ -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

View File

@ -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

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.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

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.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

View File

@ -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)

View File

@ -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) {

View File

@ -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) {

View File

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