Fix all ktlint violations and merge issues

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta 2026-01-26 23:01:37 +08:00
parent 394a7f68cd
commit 2376a83e0c
55 changed files with 482 additions and 338 deletions

View File

@ -6,6 +6,8 @@ import android.os.Parcelable
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import java.io.IOException
import java.net.SocketTimeoutException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@ -16,8 +18,6 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.exceptions.ParsingException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import java.io.IOException
import java.net.SocketTimeoutException
/**
* Instrumented tests for {@link ErrorInfo}.

View File

@ -7,6 +7,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import java.net.UnknownHostException
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -16,7 +17,6 @@ import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.ui.theme.AppTheme
import java.net.UnknownHostException
@RunWith(AndroidJUnit4::class)
class ErrorPanelTest {

View File

@ -34,6 +34,7 @@ import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import java.net.UnknownHostException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -54,7 +55,6 @@ 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.viewmodels.util.Resource
import java.net.UnknownHostException
class CommentSectionInstrumentedTest {
@ -277,6 +277,7 @@ private fun TestCommentSection(
is Resource.Loading -> item {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
is Resource.Success -> {
val commentInfo = (uiState as Resource.Success<CommentInfo>).data
val count = commentInfo.commentCount
@ -290,6 +291,7 @@ private fun TestCommentSection(
.heightIn(min = 128.dp)
)
}
count == 0 -> item {
EmptyStateComposable(
spec = EmptyStateSpec.NoComments,
@ -298,6 +300,7 @@ private fun TestCommentSection(
.heightIn(min = 128.dp)
)
}
else -> {
if (count >= 0) {
item {
@ -314,6 +317,7 @@ private fun TestCommentSection(
is LoadState.Loading -> item {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
is LoadState.Error -> item {
Box(
modifier = Modifier.fillMaxWidth()
@ -329,6 +333,7 @@ private fun TestCommentSection(
)
}
}
else -> items(comments.itemCount) { index ->
Comment(comment = comments[index]!!) {}
}
@ -336,6 +341,7 @@ private fun TestCommentSection(
}
}
}
is Resource.Error -> item {
Box(
modifier = Modifier.fillMaxSize(),

View File

@ -22,6 +22,9 @@ 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 java.io.IOException
import java.io.InterruptedIOException
import java.net.SocketException
import org.acra.ACRA.init
import org.acra.ACRA.isACRASenderServiceProcess
import org.acra.config.CoreConfigurationBuilder
@ -38,9 +41,6 @@ import org.schabi.newpipe.util.StateSaver
import org.schabi.newpipe.util.image.ImageStrategy
import org.schabi.newpipe.util.image.PreferredImageQuality
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
import java.io.IOException
import java.io.InterruptedIOException
import java.net.SocketException
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
@ -101,7 +101,7 @@ open class App :
NewPipe.init(
getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this),
Localization.getPreferredContentCountry(this)
)
Localization.initPrettyTime(Localization.resolvePrettyTime())
@ -118,9 +118,9 @@ open class App :
this,
prefs.getString(
getString(R.string.image_quality_key),
getString(R.string.image_quality_default),
),
),
getString(R.string.image_quality_default)
)
)
)
configureRxJavaErrorHandler()
@ -128,15 +128,14 @@ open class App :
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
}
override fun newImageLoader(context: Context): ImageLoader =
ImageLoader
.Builder(this)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.crossfade(true)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
}.build()
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
.Builder(this)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.crossfade(true)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
}.build()
protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null)
@ -190,7 +189,7 @@ open class App :
IOException::class.java,
SocketException::class.java, // blocking code disposed
InterruptedException::class.java,
InterruptedIOException::class.java,
InterruptedIOException::class.java
)
}
@ -204,7 +203,7 @@ open class App :
OnErrorNotImplementedException::class.java,
MissingBackpressureException::class.java,
// bug in operator
IllegalStateException::class.java,
IllegalStateException::class.java
)
}
@ -215,7 +214,7 @@ open class App :
.uncaughtExceptionHandler
.uncaughtException(Thread.currentThread(), throwable)
}
},
}
)
}
@ -241,7 +240,7 @@ open class App :
NotificationChannelCompat
.Builder(
getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW,
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build()
@ -249,7 +248,7 @@ open class App :
NotificationChannelCompat
.Builder(
getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW,
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description))
.build()
@ -257,7 +256,7 @@ open class App :
NotificationChannelCompat
.Builder(
getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH,
NotificationManagerCompat.IMPORTANCE_HIGH
).setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build()
@ -265,7 +264,7 @@ open class App :
NotificationChannelCompat
.Builder(
getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW,
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build()
@ -273,7 +272,7 @@ open class App :
NotificationChannelCompat
.Builder(
getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT,
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.streams_notification_channel_name))
.setDescription(getString(R.string.streams_notification_channel_description))
.build()

View File

@ -6,6 +6,7 @@ import androidx.annotation.StringRes
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.Loader
import java.net.UnknownHostException
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
@ -28,7 +29,6 @@ import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.player.mediasource.FailedMediaSource
import org.schabi.newpipe.player.resolver.PlaybackResolver
import org.schabi.newpipe.util.Localization
import java.net.UnknownHostException
/**
* An error has occurred in the app. This class contains plain old parcelable data that can be used
@ -59,7 +59,7 @@ class ErrorInfo private constructor(
* If present, this resource can alternatively be opened in browser (useful if NewPipe is
* badly broken).
*/
val openInBrowserUrl: String?,
val openInBrowserUrl: String?
) : Parcelable {
@JvmOverloads
@ -68,7 +68,7 @@ class ErrorInfo private constructor(
userAction: UserAction,
request: String,
serviceId: Int? = null,
openInBrowserUrl: String? = null,
openInBrowserUrl: String? = null
) : this(
throwableToStringList(throwable),
userAction,
@ -78,7 +78,7 @@ class ErrorInfo private constructor(
isReportable(throwable),
isRetryable(throwable),
(throwable as? ReCaptchaException)?.url,
openInBrowserUrl,
openInBrowserUrl
)
@JvmOverloads
@ -87,7 +87,7 @@ class ErrorInfo private constructor(
userAction: UserAction,
request: String,
serviceId: Int? = null,
openInBrowserUrl: String? = null,
openInBrowserUrl: String? = null
) : this(
throwableListToStringList(throwables),
userAction,
@ -97,7 +97,7 @@ class ErrorInfo private constructor(
throwables.any(::isReportable),
throwables.isEmpty() || throwables.any(::isRetryable),
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
openInBrowserUrl,
openInBrowserUrl
)
// constructor to manually build ErrorInfo when no throwable is available
@ -118,7 +118,7 @@ class ErrorInfo private constructor(
throwable: Throwable,
userAction: UserAction,
request: String,
info: Info?,
info: Info?
) :
this(throwable, userAction, request, info?.serviceId, info?.url)
@ -127,7 +127,7 @@ class ErrorInfo private constructor(
throwables: List<Throwable>,
userAction: UserAction,
request: String,
info: Info?,
info: Info?
) :
this(throwables, userAction, request, info?.serviceId, info?.url)
@ -144,7 +144,7 @@ class ErrorInfo private constructor(
class ErrorMessage(
@StringRes
private val stringRes: Int,
private vararg val formatArgs: String,
private vararg val formatArgs: String
) : Parcelable {
fun getString(context: Context): String {
// use Localization.compatGetString() just in case context is not AppCompatActivity
@ -158,21 +158,19 @@ class ErrorInfo private constructor(
const val SERVICE_NONE = "<unknown_service>"
private fun getServiceName(serviceId: Int?) =
// not using getNameOfServiceById since we want to accept a nullable serviceId and we
private fun getServiceName(serviceId: Int?) = // not using getNameOfServiceById since we want to accept a nullable serviceId and we
// want to default to SERVICE_NONE
ServiceList.all()?.firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
?: SERVICE_NONE
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
fun throwableListToStringList(throwableList: List<Throwable>) =
throwableList.map { it.stackTraceToString() }.toTypedArray()
fun throwableListToStringList(throwableList: List<Throwable>) = throwableList.map { it.stackTraceToString() }.toTypedArray()
fun getMessage(
throwable: Throwable?,
action: UserAction?,
serviceId: Int?,
serviceId: Int?
): ErrorMessage {
return when {
// player exceptions
@ -191,18 +189,24 @@ class ErrorInfo private constructor(
ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString())
}
}
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException ->
getMessage(throwable, action, serviceId)
throwable.type == ExoPlaybackException.TYPE_SOURCE ->
ErrorMessage(R.string.player_stream_failure)
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED ->
ErrorMessage(R.string.player_recoverable_failure)
else ->
ErrorMessage(R.string.player_unrecoverable_failure)
}
}
throwable is FailedMediaSource.FailedMediaSourceException ->
getMessage(throwable.cause, action, serviceId)
throwable is PlaybackResolver.ResolverException ->
ErrorMessage(R.string.player_stream_failure)
@ -218,34 +222,46 @@ class ErrorInfo private constructor(
)
}
?: ErrorMessage(R.string.account_terminated)
throwable is AgeRestrictedContentException ->
ErrorMessage(R.string.restricted_video_no_stream)
throwable is GeographicRestrictionException ->
ErrorMessage(R.string.georestricted_content)
throwable is PaidContentException ->
ErrorMessage(R.string.paid_content)
throwable is PrivateContentException ->
ErrorMessage(R.string.private_content)
throwable is SoundCloudGoPlusContentException ->
ErrorMessage(R.string.soundcloud_go_plus_content)
throwable is UnsupportedContentInCountryException ->
ErrorMessage(R.string.unsupported_content_in_country)
throwable is YoutubeMusicPremiumContentException ->
ErrorMessage(R.string.youtube_music_premium_content)
throwable is SignInConfirmNotBotException ->
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
throwable is ContentNotAvailableException ->
ErrorMessage(R.string.content_not_available)
// other extractor exceptions
throwable is ContentNotSupportedException ->
ErrorMessage(R.string.content_not_supported)
// ReCaptchas will be handled in a special way anyway
throwable is ReCaptchaException ->
ErrorMessage(R.string.recaptcha_request_toast)
// test this at the end as many exceptions could be a subclass of IOException
throwable != null && throwable.isNetworkRelated ->
ErrorMessage(R.string.network_error)
// an extraction exception unrelated to the network
// is likely an issue with parsing the website
throwable is ExtractionException ->
@ -254,16 +270,22 @@ class ErrorInfo private constructor(
// user actions (in case the exception is null or unrecognizable)
action == UserAction.UI_ERROR ->
ErrorMessage(R.string.app_ui_crash)
action == UserAction.REQUESTED_COMMENTS ->
ErrorMessage(R.string.error_unable_to_load_comments)
action == UserAction.SUBSCRIPTION_CHANGE ->
ErrorMessage(R.string.subscription_change_failed)
action == UserAction.SUBSCRIPTION_UPDATE ->
ErrorMessage(R.string.subscription_update_failed)
action == UserAction.LOAD_IMAGE ->
ErrorMessage(R.string.could_not_load_thumbnails)
action == UserAction.DOWNLOAD_OPEN_DIALOG ->
ErrorMessage(R.string.could_not_setup_download_menu)
else ->
ErrorMessage(R.string.error_snackbar_message)
}
@ -274,18 +296,23 @@ class ErrorInfo private constructor(
// we don't have an exception, so this is a manually built error, which likely
// indicates that it's important and is thus reportable
null -> true
// a recaptcha was detected, and the user needs to solve it, there is no use in
// letting users report it
is ReCaptchaException -> false
// the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false
// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
// happens often when there is no internet connection; we don't use
// `throwable.isNetworkRelated` since any `IOException` would make that function
// return true, but not all `IOException`s are network related
is UnknownHostException -> false
// by default, this is an unexpected exception, which the user could report
else -> true
}
@ -295,8 +322,10 @@ class ErrorInfo private constructor(
return when (throwable) {
// we know the content is not available, retrying won't help
is ContentNotAvailableException -> false
// we know the content is not supported, retrying won't help
is ContentNotSupportedException -> false
// by default (including if throwable is null), enable retrying (though the retry
// button will be shown only if a way to perform the retry is implemented)
else -> true

View File

@ -56,6 +56,11 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.LinkedList
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.model.StreamEntity
@ -115,11 +120,6 @@ import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.KoreUtils
import org.schabi.newpipe.util.external_communication.ShareUtils
import org.schabi.newpipe.util.image.CoilHelper
import java.util.LinkedList
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class VideoDetailFragment :
BaseStateFragment<StreamInfo>(),
@ -128,15 +128,29 @@ class VideoDetailFragment :
OnKeyDownListener {
// stream info
@JvmField @State var serviceId: Int = NO_SERVICE_ID
@JvmField @State var title: String = ""
@JvmField @State var url: String? = null
@JvmField
@State
var serviceId: Int = NO_SERVICE_ID
@JvmField
@State
var title: String = ""
@JvmField
@State
var url: String? = null
private var currentInfo: StreamInfo? = null
// player objects
private var playQueue: PlayQueue? = null
@JvmField @State var autoPlayEnabled: Boolean = true
@JvmField @State var originalOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
@JvmField
@State
var autoPlayEnabled: Boolean = true
@JvmField
@State
var originalOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var playerService: PlayerService? = null
private var player: Player? = null
@ -152,7 +166,9 @@ class VideoDetailFragment :
private var showRelatedItems = false
private var showDescription = false
private lateinit var selectedTabTag: String
@AttrRes val tabIcons = ArrayList<Int>()
@StringRes val tabContentDescriptions = ArrayList<Int>()
private var tabSettingsChanged = false
private var lastAppBarVerticalOffset = Int.Companion.MAX_VALUE // prevents useless updates
@ -172,8 +188,13 @@ class VideoDetailFragment :
}
// bottom sheet
@JvmField @State var bottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
@JvmField @State var lastStableBottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
@JvmField
@State
var bottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
@JvmField
@State
var lastStableBottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
private lateinit var bottomSheetBehavior: BottomSheetBehavior<FrameLayout?>
private lateinit var bottomSheetCallback: BottomSheetCallback
private lateinit var broadcastReceiver: BroadcastReceiver
@ -244,7 +265,8 @@ class VideoDetailFragment :
showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true)
showDescription = prefs.getBoolean(getString(R.string.show_description_key), true)
selectedTabTag = prefs.getString(
getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG
getString(R.string.stream_info_selected_tab_key),
COMMENTS_TAB_TAG
)!!
prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
@ -258,7 +280,8 @@ class VideoDetailFragment :
}
}
activity.contentResolver.registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
false,
settingsContentObserver!!
)
}
@ -357,7 +380,13 @@ class VideoDetailFragment :
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
NavigationHelper.openVideoDetailFragment(
requireContext(), getFM(), serviceId, url, title, null, false
requireContext(),
getFM(),
serviceId,
url,
title,
null,
false
)
} else {
Log.e(TAG, "ReCaptcha failed")
@ -562,7 +591,7 @@ class VideoDetailFragment :
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
binding.detailControlsCrashThePlayer.isVisible =
DEBUG && PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.show_crash_the_player_key), false)
.getBoolean(getString(R.string.show_crash_the_player_key), false)
accommodateForTvAndDesktopMode()
}
@ -845,7 +874,9 @@ class VideoDetailFragment :
private fun updateTabs(info: StreamInfo) {
if (showRelatedItems) {
when (val relatedItemsLayout = binding.relatedItemsLayout) {
null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info)) // phone
null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info))
// phone
else -> { // tablet + TV
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, getInstance(info))
@ -896,7 +927,9 @@ class VideoDetailFragment :
val viewPagerVisibleHeight = height - pagerHitRect.top
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
val tabLayoutHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics
TypedValue.COMPLEX_UNIT_DIP,
48f,
resources.displayMetrics
)
if (viewPagerVisibleHeight > tabLayoutHeight * 2) {
@ -997,7 +1030,7 @@ class VideoDetailFragment :
}
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)
) {
showExternalVideoPlaybackDialog()
} else {
@ -1046,7 +1079,10 @@ class VideoDetailFragment :
tryAddVideoPlayerView()
val playerIntent = NavigationHelper.getPlayerIntent(
requireContext(), PlayerService::class.java, queue, PlayerIntentType.AllOthers
requireContext(),
PlayerService::class.java,
queue,
PlayerIntentType.AllOthers
)
.putExtra(Player.PLAY_WHEN_READY, autoPlayEnabled)
.putExtra(Player.RESUME_PLAYBACK, true)
@ -1102,7 +1138,10 @@ class VideoDetailFragment :
selectedStream: Stream
) {
NavigationHelper.playOnExternalPlayer(
context, info.name, info.subChannelName, selectedStream
context,
info.name,
info.subChannelName,
selectedStream
)
val recordManager = HistoryRecordManager(requireContext())
@ -1173,10 +1212,11 @@ class VideoDetailFragment :
private val preDrawListener: OnPreDrawListener = OnPreDrawListener {
view?.let { view ->
val decorView = if (DeviceUtils.isInMultiWindow(activity))
val decorView = if (DeviceUtils.isInMultiWindow(activity)) {
view
else
} else {
activity.window.decorView
}
setHeightThumbnail(decorView.height, resources.displayMetrics)
view.getViewTreeObserver().removeOnPreDrawListener(preDrawListener)
}
@ -1196,10 +1236,11 @@ class VideoDetailFragment :
if (this.isFullscreen) {
val height = (
if (DeviceUtils.isInMultiWindow(activity))
if (DeviceUtils.isInMultiWindow(activity)) {
requireView()
else
} else {
activity.window.decorView
}
).height
// Height is zero when the view is not yet displayed like after orientation change
if (height != 0) {
@ -1210,10 +1251,11 @@ class VideoDetailFragment :
} else {
val isPortrait = metrics.heightPixels > metrics.widthPixels
val height = (
if (isPortrait)
if (isPortrait) {
metrics.widthPixels / (16.0f / 9.0f)
else
} else {
metrics.heightPixels / 2.0f
}
).toInt()
setHeightThumbnail(height, metrics)
}
@ -1288,7 +1330,9 @@ class VideoDetailFragment :
override fun onReceive(context: Context?, intent: Intent) {
when (intent.action) {
ACTION_SHOW_MAIN_PLAYER -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
ACTION_HIDE_MAIN_PLAYER -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
ACTION_PLAYER_STARTED -> {
// If the state is not hidden we don't need to show the mini player
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
@ -1446,8 +1490,10 @@ class VideoDetailFragment :
checkUpdateProgressInfo(info)
CoilHelper.loadDetailsThumbnail(binding.detailThumbnailImageView, info.thumbnails)
ExtractorHelper.showMetaInfoInTextView(
info.metaInfo, binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables
info.metaInfo,
binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator,
disposables
)
if (playerIsStopped) {
@ -1561,7 +1607,9 @@ class VideoDetailFragment :
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ state -> updatePlaybackProgress(state.progressMillis, info.duration * 1000) },
{ throwable -> /* impossible due to the onErrorComplete() */ },
{ throwable ->
/* impossible due to the onErrorComplete() */
},
{
/* onComplete */
binding.positionView.visibility = View.GONE
@ -1607,7 +1655,7 @@ class VideoDetailFragment :
Log.d(
TAG,
"onQueueUpdate() called with: serviceId = [$serviceId], url = [${
url}], name = [$title], playQueue = [$playQueue]"
url}], name = [$title], playQueue = [$playQueue]"
)
}
@ -1787,7 +1835,8 @@ class VideoDetailFragment :
activity.window.decorView.systemUiVisibility = 0
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
activity.window.statusBarColor = ThemeHelper.resolveColorFromAttr(
requireContext(), android.R.attr.colorPrimary
requireContext(),
android.R.attr.colorPrimary
)
}
@ -2013,7 +2062,8 @@ class VideoDetailFragment :
if (audioTracks.isEmpty()) {
Toast.makeText(
activity, R.string.no_audio_streams_available_for_external_players,
activity,
R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT
).show()
} else if (audioTracks.size == 1) {
@ -2053,6 +2103,7 @@ class VideoDetailFragment :
/*//////////////////////////////////////////////////////////////////////////
// Bottom mini player
////////////////////////////////////////////////////////////////////////// */
/**
* That's for Android TV support. Move focus from main fragment to the player or back
* based on what is currently selected
@ -2311,6 +2362,7 @@ class VideoDetailFragment :
/*//////////////////////////////////////////////////////////////////////////
// OwnStack
////////////////////////////////////////////////////////////////////////// */
/**
* Stack that contains the "navigation history".<br></br>
* The peek is the current video.

View File

@ -37,7 +37,10 @@ class StreamSegmentItem(
viewBinding.textViewStartSeconds.text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
viewBinding.root.setOnLongClickListener {
onClick.onItemLongClick(this, item.startTimeSeconds)
true
}
viewBinding.root.isSelected = isSelected
}

View File

@ -9,5 +9,5 @@ inline fun Bitmap.scale(
width: Int,
height: Int,
srcRect: Rect? = null,
scaleInLinearSpace: Boolean = true,
scaleInLinearSpace: Boolean = true
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)

View File

@ -6,6 +6,8 @@ import android.view.View
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.xwray.groupie.viewbinding.BindableItem
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.StreamWithState
@ -20,8 +22,6 @@ import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.CoilHelper
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
data class StreamItem(
val streamWithState: StreamWithState,
@ -132,6 +132,7 @@ data class StreamItem(
viewsAndDate.isEmpty() -> uploadDate!!
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
}
else -> viewsAndDate
}
}

View File

@ -231,7 +231,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val data = result.data?.dataString
if (data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
this, SubscriptionImportInput.PreviousExportMode(data)
this,
SubscriptionImportInput.PreviousExportMode(data)
)
}
}

View File

@ -19,13 +19,13 @@
package org.schabi.newpipe.local.subscription.workers
import java.io.InputStream
import java.io.OutputStream
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException
import java.io.InputStream
import java.io.OutputStream
/**
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
@ -65,7 +65,7 @@ object ImportExportJsonHelper {
@JvmStatic
fun writeTo(
items: List<SubscriptionItem>,
out: OutputStream,
out: OutputStream
) {
json.encodeToStream(SubscriptionData(items), out)
}

View File

@ -25,7 +25,7 @@ import org.schabi.newpipe.R
class SubscriptionExportWorker(
appContext: Context,
params: WorkerParameters,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
// This is needed for API levels < 31 (Android S).
override suspend fun getForegroundInfo(): ForegroundInfo {
@ -102,7 +102,7 @@ class SubscriptionExportWorker(
fun schedule(
context: Context,
uri: Uri,
uri: Uri
) {
val data = workDataOf(EXPORT_PATH to uri.toString())
val workRequest =

View File

@ -31,7 +31,7 @@ import org.schabi.newpipe.util.ExtractorHelper
class SubscriptionImportWorker(
appContext: Context,
params: WorkerParameters,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
// This is needed for API levels < 31 (Android S).
override suspend fun getForegroundInfo(): ForegroundInfo {
@ -139,7 +139,7 @@ class SubscriptionImportWorker(
title: String,
text: String?,
currentProgress: Int,
maxProgress: Int,
maxProgress: Int
): ForegroundInfo {
val notification =
NotificationCompat
@ -154,7 +154,7 @@ class SubscriptionImportWorker(
.addAction(
R.drawable.ic_close,
applicationContext.getString(R.string.cancel),
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id),
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
).apply {
if (currentProgress > 0 && maxProgress > 0) {
val progressText = "$currentProgress/$maxProgress"
@ -187,8 +187,10 @@ class SubscriptionImportWorker(
sealed class SubscriptionImportInput : Parcelable {
@Parcelize
data class ChannelUrlMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
@Parcelize
data class InputStreamMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
@Parcelize
data class PreviousExportMode(val url: String) : SubscriptionImportInput()
@ -218,6 +220,7 @@ sealed class SubscriptionImportInput : Parcelable {
val url = data.getString("url")!!
return ChannelUrlMode(serviceId, url)
}
INPUT_STREAM_MODE -> {
val serviceId = data.getInt("service_id", -1)
if (serviceId == -1) {
@ -226,10 +229,12 @@ sealed class SubscriptionImportInput : Parcelable {
val url = data.getString("url")!!
return InputStreamMode(serviceId, url)
}
PREVIOUS_EXPORT_MODE -> {
val url = data.getString("url")!!
return PreviousExportMode(url)
}
else -> throw IllegalArgumentException("Unknown mode: $mode")
}
}

View File

@ -10,7 +10,7 @@ import org.schabi.newpipe.extractor.comments.CommentsInfo
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
class CommentRepliesSource(
private val commentInfo: CommentsInfoItem,
private val commentInfo: CommentsInfoItem
) : PagingSource<Page, CommentsInfoItem>() {
private val service = NewPipe.getService(commentInfo.serviceId)

View File

@ -29,6 +29,8 @@ import android.util.Log
import androidx.core.app.ServiceCompat
import androidx.media.MediaBrowserServiceCompat
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import java.lang.ref.WeakReference
import java.util.function.Consumer
import org.schabi.newpipe.ktx.toDebugString
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer
@ -36,8 +38,6 @@ import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
import org.schabi.newpipe.player.notification.NotificationPlayerUi
import org.schabi.newpipe.player.notification.NotificationUtil
import org.schabi.newpipe.util.ThemeHelper
import java.lang.ref.WeakReference
import java.util.function.Consumer
/**
* One service for all players.
@ -110,7 +110,7 @@ class PlayerService : MediaBrowserServiceCompat() {
Log.d(
TAG,
"onStartCommand() called with: intent = [$intent], extras = [${
intent.extras.toDebugString()}], flags = [$flags], startId = [$startId]"
intent.extras.toDebugString()}], flags = [$flags], startId = [$startId]"
)
}
@ -251,7 +251,7 @@ class PlayerService : MediaBrowserServiceCompat() {
Log.d(
TAG,
"onBind() called with: intent = [$intent], extras = [${
intent.extras.toDebugString()}]"
intent.extras.toDebugString()}]"
)
}

View File

@ -43,13 +43,13 @@ object PlayerHolder {
private val playQueue: PlayQueue?
get() = this.player?.playQueue
/**
* Returns the current [PlayerType] of the [PlayerService] service,
* otherwise `null` if no service is running.
*
* @return Current PlayerType
*/
val type: PlayerType?
/**
* Returns the current [PlayerType] of the [PlayerService] service,
* otherwise `null` if no service is running.
*
* @return Current PlayerType
*/
get() = this.player?.playerType
val isPlaying: Boolean
@ -58,12 +58,12 @@ object PlayerHolder {
val isPlayerOpen: Boolean
get() = this.player != null
/**
* Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
* the stream long press menu) when there actually is a play queue to manipulate.
* @return true only if the player is open and its play queue is ready (i.e. it is not null)
*/
val isPlayQueueReady: Boolean
/**
* Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
* the stream long press menu) when there actually is a play queue to manipulate.
* @return true only if the player is open and its play queue is ready (i.e. it is not null)
*/
get() = this.playQueue != null
val queueSize: Int

View File

@ -47,7 +47,7 @@ import org.schabi.newpipe.util.image.ImageStrategy
*/
class MediaBrowserImpl(
private val context: Context,
notifyChildrenChanged: (parentId: String) -> Unit,
notifyChildrenChanged: (parentId: String) -> Unit
) {
private val packageValidator = PackageValidator(context)
private val database = NewPipeDatabase.getInstance(context)
@ -87,7 +87,8 @@ class MediaBrowserImpl(
val extras = Bundle()
extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED,
true
)
return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras)
}
@ -135,7 +136,7 @@ class MediaBrowserImpl(
)
}
when (/*val uriType = */path.removeAt(0)) {
when (path.removeAt(0)) {
ID_BOOKMARKS -> {
if (path.isEmpty()) {
return populateBookmarks()
@ -206,7 +207,7 @@ class MediaBrowserImpl(
return MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
@ -260,7 +261,7 @@ class MediaBrowserImpl(
private fun createLocalPlaylistStreamMediaItem(
playlistId: Long,
item: PlaylistStreamEntry,
index: Int,
index: Int
): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder()
.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
@ -277,7 +278,7 @@ class MediaBrowserImpl(
private fun createRemotePlaylistStreamMediaItem(
playlistId: Long,
item: StreamInfoItem,
index: Int,
index: Int
): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder()
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
@ -294,7 +295,7 @@ class MediaBrowserImpl(
private fun createMediaIdForPlaylistIndex(
isRemote: Boolean,
playlistId: Long,
index: Int,
index: Int
): String {
return buildLocalPlaylistItemMediaId(isRemote, playlistId)
.appendPath(index.toString())

View File

@ -4,6 +4,9 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.subjects.PublishSubject
import java.io.Serializable
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent
@ -12,9 +15,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RecoveryEvent
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent
import java.io.Serializable
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
/**
* PlayQueue is responsible for keeping track of a list of streams and the index of
@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger
*/
abstract class PlayQueue internal constructor(
index: Int,
startWith: List<PlayQueueItem>,
startWith: List<PlayQueueItem>
) : Serializable {
private val queueIndex = AtomicInteger(index)
private val history = mutableListOf<PlayQueueItem>()
@ -105,22 +105,20 @@ abstract class PlayQueue internal constructor(
/*//////////////////////////////////////////////////////////////////////////
// Readonly ops
////////////////////////////////////////////////////////////////////////// */
/**
* Changes the current playing index to a new index.
*
* This method is guarded using in a circular manner for index exceeding the play queue size.
*
* Will emit a [SelectEvent] if the index is not the current playing index.
*
* @param index the index to be set
* @return the current index that should be played
*/
@set:Synchronized
var index: Int = 0
/**
* @return the current index that should be played
*/
get() = queueIndex.get()
/**
* Changes the current playing index to a new index.
*
* This method is guarded using in a circular manner for index exceeding the play queue size.
*
* Will emit a [SelectEvent] if the index is not the current playing index.
*
* @param index the index to be set
*/
set(index) {
val oldIndex = field
@ -340,7 +338,7 @@ abstract class PlayQueue internal constructor(
@Synchronized
fun move(
source: Int,
target: Int,
target: Int
) {
if (source < 0 || target < 0) {
return
@ -375,7 +373,7 @@ abstract class PlayQueue internal constructor(
@Synchronized
fun setRecovery(
index: Int,
position: Long,
position: Long
) {
streams.getOrNull(index)?.let {
it.recoveryPosition = position

View File

@ -2,13 +2,13 @@ package org.schabi.newpipe.player.playqueue
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import java.io.Serializable
import java.util.Objects
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.ExtractorHelper
import java.io.Serializable
import java.util.Objects
class PlayQueueItem private constructor(
val title: String,
@ -18,7 +18,7 @@ class PlayQueueItem private constructor(
val thumbnails: List<Image>,
val uploader: String,
val uploaderUrl: String?,
val streamType: StreamType,
val streamType: StreamType
) : Serializable {
//
// ////////////////////////////////////////////////////////////////////// */
@ -40,7 +40,7 @@ class PlayQueueItem private constructor(
info.thumbnails,
info.uploaderName.orEmpty(),
info.uploaderUrl,
info.streamType,
info.streamType
) {
if (info.startPosition > 0) {
this.recoveryPosition = info.startPosition * 1000
@ -55,7 +55,7 @@ class PlayQueueItem private constructor(
item.thumbnails,
item.uploaderName.orEmpty(),
item.uploaderUrl,
item.streamType,
item.streamType
)
val stream: Single<StreamInfo>

View File

@ -1,8 +1,8 @@
package org.schabi.newpipe.player.ui
import org.schabi.newpipe.util.GuardedByMutex
import kotlin.reflect.KClass
import kotlin.reflect.safeCast
import org.schabi.newpipe.util.GuardedByMutex
/**
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
@ -78,22 +78,20 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param T the class type parameter
* @return the first player UI of the required type found in the list, or null
</T> */
fun <T : PlayerUi> get(playerUiType: KClass<T>): T? =
playerUis.runWithLockSync {
for (ui in lockData) {
if (playerUiType.isInstance(ui)) {
// try all UIs before returning null
playerUiType.safeCast(ui)?.let { return@runWithLockSync it }
}
fun <T : PlayerUi> get(playerUiType: KClass<T>): T? = playerUis.runWithLockSync {
for (ui in lockData) {
if (playerUiType.isInstance(ui)) {
// try all UIs before returning null
playerUiType.safeCast(ui)?.let { return@runWithLockSync it }
}
return@runWithLockSync null
}
return@runWithLockSync null
}
/**
* See [get] above
*/
fun <T : PlayerUi> get(playerUiType: Class<T>): T? =
get(playerUiType.kotlin)
fun <T : PlayerUi> get(playerUiType: Class<T>): T? = get(playerUiType.kotlin)
/**
* Calls the provided consumer on all player UIs in the list, in order of addition.

View File

@ -13,7 +13,6 @@ import org.schabi.newpipe.ui.theme.SizeTokens
@Composable
fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) {
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
Column(modifier = modifier) {

View File

@ -5,15 +5,15 @@ import com.grack.nanojson.JsonArray
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteIfExists
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
class ImportExportManager(private val fileLocator: BackupFileLocator) {
companion object {
@ -117,10 +117,15 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
for ((key, value) in entries) {
when (value) {
is Boolean -> editor.putBoolean(key, value)
is Float -> editor.putFloat(key, value)
is Int -> editor.putInt(key, value)
is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value)
is Set<*> -> {
// There are currently only Sets with type String possible
@Suppress("UNCHECKED_CAST")
@ -154,10 +159,15 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
for ((key, value) in jsonObject) {
when (value) {
is Boolean -> editor.putBoolean(key, value)
is Float -> editor.putFloat(key, value)
is Int -> editor.putInt(key, value)
is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value)
is JsonArray -> {
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
}

View File

@ -6,11 +6,11 @@ import android.content.SharedPreferences
import androidx.lifecycle.AndroidViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
@ -18,7 +18,7 @@ class SettingsViewModel @Inject constructor(
private val preferenceManager: SharedPreferences
) : AndroidViewModel(context.applicationContext as Application) {
private var _settingsLayoutRedesignPref: Boolean
private var settingsLayoutRedesignPref: Boolean
get() = preferenceManager.getBoolean(
Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key),
false
@ -30,11 +30,11 @@ class SettingsViewModel @Inject constructor(
).apply()
}
private val _settingsLayoutRedesign: MutableStateFlow<Boolean> =
MutableStateFlow(_settingsLayoutRedesignPref)
MutableStateFlow(settingsLayoutRedesignPref)
val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow()
fun toggleSettingsLayoutRedesign(newState: Boolean) {
_settingsLayoutRedesign.value = newState
_settingsLayoutRedesignPref = newState
settingsLayoutRedesignPref = newState
}
}

View File

@ -36,14 +36,14 @@ fun SwitchPreference(
text = stringResource(id = title),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Start,
textAlign = TextAlign.Start
)
summary?.let {
Text(
text = stringResource(id = summary),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Start,
textAlign = TextAlign.Start
)
}
}

View File

@ -28,7 +28,7 @@ fun TextPreference(
@StringRes title: Int,
@DrawableRes icon: Int? = null,
@StringRes summary: Int? = null,
onClick: () -> Unit,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -51,14 +51,14 @@ fun TextPreference(
text = stringResource(id = title),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Start,
textAlign = TextAlign.Start
)
summary?.let {
Text(
text = stringResource(id = summary),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Start,
textAlign = TextAlign.Start
)
}
}

View File

@ -37,7 +37,8 @@ fun TextAction(text: String, modifier: Modifier = Modifier) {
@Composable
fun NavigationIcon() {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back",
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
)
}

View File

@ -37,20 +37,28 @@ import org.schabi.newpipe.util.image.NewPipeSquircleIcon
private val ABOUT_ITEMS = listOf(
AboutData(R.string.faq_title, R.string.faq_description, R.string.faq, R.string.faq_url),
AboutData(
R.string.contribution_title, R.string.contribution_encouragement,
R.string.view_on_github, R.string.github_url
R.string.contribution_title,
R.string.contribution_encouragement,
R.string.view_on_github,
R.string.github_url
),
AboutData(
R.string.donation_title, R.string.donation_encouragement, R.string.give_back,
R.string.donation_title,
R.string.donation_encouragement,
R.string.give_back,
R.string.donation_url
),
AboutData(
R.string.website_title, R.string.website_encouragement, R.string.open_in_browser,
R.string.website_title,
R.string.website_encouragement,
R.string.open_in_browser,
R.string.website_url
),
AboutData(
R.string.privacy_policy_title, R.string.privacy_policy_encouragement,
R.string.read_privacy_policy, R.string.privacy_policy_url
R.string.privacy_policy_title,
R.string.privacy_policy_encouragement,
R.string.read_privacy_policy,
R.string.privacy_policy_url
)
)
@ -86,23 +94,23 @@ fun AboutTab() {
Image(
imageVector = NewPipeSquircleIcon,
contentDescription = stringResource(R.string.app_name),
modifier = Modifier.size(64.dp),
modifier = Modifier.size(64.dp)
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
textAlign = TextAlign.Center
)
Text(
text = BuildConfig.VERSION_NAME,
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.app_description),
textAlign = TextAlign.Center,
textAlign = TextAlign.Center
)
}
@ -120,7 +128,7 @@ fun AboutTab() {
@NonRestartableComposable
private fun AboutItem(
@PreviewParameter(AboutDataProvider::class) aboutData: AboutData,
modifier: Modifier = Modifier,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(

View File

@ -39,7 +39,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
fun Library(
@PreviewParameter(LibraryProvider::class) library: Library,
showLicenseDialog: (licenseFilename: String) -> Unit,
descriptionMaxLines: Int,
descriptionMaxLines: Int
) {
val spdxLicense = library.licenses.firstOrNull()?.spdxId?.takeIf { it.isNotBlank() }
val licenseAssetPath = spdxLicense?.let { SPDX_ID_TO_ASSET_PATH[it] }
@ -63,14 +63,14 @@ fun Library(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = library.name,
modifier = Modifier.weight(0.75f),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis
)
val version = library.artifactVersion
if (!version.isNullOrBlank()) {
@ -85,7 +85,7 @@ fun Library(
}.padding(start = 8.dp),
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis
)
}
}
@ -95,7 +95,7 @@ fun Library(
text = author,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis
)
}
val description = library.description
@ -105,14 +105,14 @@ fun Library(
text = description,
style = MaterialTheme.typography.bodySmall,
maxLines = descriptionMaxLines,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis
)
}
if (library.licenses.isNotEmpty()) {
FlowRow(
modifier = Modifier.padding(top = 6.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
library.licenses.forEach {
Badge {

View File

@ -1,4 +1,4 @@
/**
/*
* The library definitions for most libraries are autogenerated by the AboutLibraries plugin.
* This file is only for TeamNewPipe-related libraries.
*/
@ -22,12 +22,12 @@ val SPDX_ID_TO_ASSET_PATH = mapOf(
"GPL-3.0-only" to "gpl_3.html",
"GPL-3.0-or-later" to "gpl_3.html",
"MIT" to "mit.html",
"MPL-2.0" to "mpl2.html",
"MPL-2.0" to "mpl2.html"
)
fun getFirstPartyLibraries(
context: Context,
teamNewPipeLibraries: List<Library>,
teamNewPipeLibraries: List<Library>
): List<Library> {
val gpl3 = setOf(
License(
@ -36,7 +36,7 @@ fun getFirstPartyLibraries(
year = null,
spdxId = "GPL-3.0-or-later",
licenseContent = null,
hash = "GPL-3.0-or-later",
hash = "GPL-3.0-or-later"
)
).toImmutableSet()
@ -58,7 +58,7 @@ fun getFirstPartyLibraries(
).toImmutableList(),
organization = null,
scm = Scm(null, null, context.getString(R.string.github_url)),
licenses = gpl3,
licenses = gpl3
),
Library(
uniqueId = npeId,
@ -74,15 +74,15 @@ fun getFirstPartyLibraries(
).toImmutableList(),
organization = null,
scm = Scm(null, null, context.getString(R.string.newpipe_extractor_github_url)),
licenses = gpl3,
),
licenses = gpl3
)
)
}
fun getAdditionalThirdPartyLibraries(
context: Context,
teamNewPipeLibraries: List<Library>,
licenses: ImmutableSet<License>,
licenses: ImmutableSet<License>
): List<Library> {
val apache2 = licenses.firstOrNull { it.spdxId == "Apache-2.0" }
val mit = licenses.firstOrNull { it.spdxId == "MIT" }
@ -103,7 +103,7 @@ fun getAdditionalThirdPartyLibraries(
developers = listOf(
Developer(
name = "Jonas Kalderstam",
organisationUrl = "https://github.com/spacecowboy/NoNonsense-FilePicker",
organisationUrl = "https://github.com/spacecowboy/NoNonsense-FilePicker"
),
Developer(
name = context.getString(R.string.team_newpipe),
@ -112,7 +112,7 @@ fun getAdditionalThirdPartyLibraries(
).toImmutableList(),
organization = null,
scm = Scm(null, null, "https://github.com/TeamNewPipe/NoNonsense-FilePicker"),
licenses = listOfNotNull(mpl2).toImmutableSet(),
licenses = listOfNotNull(mpl2).toImmutableSet()
),
Library(
uniqueId = nanojsonId,
@ -123,16 +123,16 @@ fun getAdditionalThirdPartyLibraries(
developers = listOf(
Developer(
name = "mmastrac",
organisationUrl = "https://github.com/mmastrac/nanojson",
organisationUrl = "https://github.com/mmastrac/nanojson"
),
Developer(
name = context.getString(R.string.team_newpipe),
organisationUrl = context.getString(R.string.website_url)
),
)
).toImmutableList(),
organization = null,
scm = Scm(null, null, "https://github.com/TeamNewPipe/nanojson"),
licenses = listOfNotNull(mit, apache2).toImmutableSet()
),
)
)
}

View File

@ -40,7 +40,7 @@ fun LicenseDialog(licenseHtml: AnnotatedString, onDismissRequest: () -> Unit) {
} else {
Text(
text = licenseHtml,
modifier = Modifier.padding(horizontal = 12.dp),
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}

View File

@ -32,7 +32,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
LazyColumnThemedScrollbar(state = lazyListState) {
LazyColumn(
state = lazyListState,
state = lazyListState
) {
item {
Text(
@ -43,7 +43,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
top = 16.dp,
end = 16.dp,
bottom = 8.dp
),
)
)
}
item {
@ -54,7 +54,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
start = 16.dp,
end = 16.dp,
bottom = 8.dp
),
)
)
}
if (state.firstPartyLibraries == null) {
@ -67,7 +67,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
Library(
library = library,
showLicenseDialog = viewModel::showLicenseDialog,
descriptionMaxLines = Int.MAX_VALUE,
descriptionMaxLines = Int.MAX_VALUE
)
}
}
@ -82,7 +82,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
top = 16.dp,
end = 16.dp,
bottom = 8.dp
),
)
)
}
if (state.thirdPartyLibraries == null) {
@ -95,7 +95,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
Library(
library = library,
showLicenseDialog = viewModel::showLicenseDialog,
descriptionMaxLines = 2,
descriptionMaxLines = 2
)
}
}

View File

@ -45,7 +45,7 @@ class LicenseTabViewModel : ViewModel() {
_state.update {
it.copy(
firstPartyLibraries = firstParty,
thirdPartyLibraries = allThirdParty,
thirdPartyLibraries = allThirdParty
)
}
}
@ -77,6 +77,6 @@ class LicenseTabViewModel : ViewModel() {
val firstPartyLibraries: List<Library>?,
val thirdPartyLibraries: List<Library>?,
// null if dialog closed, empty if loading, otherwise license HTML content
val licenseDialogHtml: AnnotatedString?,
val licenseDialogHtml: AnnotatedString?
)
}

View File

@ -29,7 +29,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
fun ErrorPanel(
errorInfo: ErrorInfo,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
onRetry: (() -> Unit)? = null
) {
val context = LocalContext.current
val isPreview = LocalInspectionMode.current
@ -42,7 +42,7 @@ fun ErrorPanel(
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier,
modifier = modifier
) {
Text(
text = messageText,
@ -97,9 +97,9 @@ private fun ErrorPanelPreview() {
throwable = ReCaptchaException("An error", "https://example.com"),
userAction = UserAction.REQUESTED_STREAM,
request = "Preview request",
openInBrowserUrl = "https://example.com",
openInBrowserUrl = "https://example.com"
),
onRetry = {},
onRetry = {}
)
}
}

View File

@ -34,7 +34,7 @@ fun ScaffoldWithToolbar(
scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
navigationIcon = {
IconButton(onClick = onBackClick) {

View File

@ -10,7 +10,7 @@ import my.nanihadesuka.compose.ScrollbarSettings
@Composable
fun defaultThemedScrollbarSettings(): ScrollbarSettings = ScrollbarSettings.Default.copy(
thumbUnselectedColor = MaterialTheme.colorScheme.primary,
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f)
)
@Composable
@ -26,6 +26,6 @@ fun LazyColumnThemedScrollbar(
modifier = modifier,
settings = settings,
indicatorContent = indicatorContent,
content = content,
content = content
)
}

View File

@ -21,7 +21,7 @@ import org.schabi.newpipe.ui.theme.SizeTokens.SpacingSmall
fun ServiceColoredButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable() RowScope.() -> Unit,
content: @Composable RowScope.() -> Unit
) {
Button(
onClick = onClick,
@ -33,9 +33,9 @@ fun ServiceColoredButton(
contentPadding = PaddingValues(horizontal = SpacingMedium, vertical = SpacingSmall),
shape = RectangleShape,
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 8.dp,
defaultElevation = 8.dp
),
)
) {
content()
}

View File

@ -40,11 +40,20 @@ fun ItemList(
val fragmentManager = context.findFragmentActivity().supportFragmentManager
if (item is StreamInfoItem) {
NavigationHelper.openVideoDetailFragment(
context, fragmentManager, item.serviceId, item.url, item.name, null, false
context,
fragmentManager,
item.serviceId,
item.url,
item.name,
null,
false
)
} else if (item is PlaylistInfoItem) {
NavigationHelper.openPlaylistFragment(
fragmentManager, item.serviceId, item.url, item.name
fragmentManager,
item.serviceId,
item.url,
item.name
)
}
}
@ -82,7 +91,12 @@ fun ItemList(
if (item is StreamInfoItem) {
val isSelected = selectedStream == item
StreamListItem(
item, showProgress, isSelected, onClick, onLongClick, onDismissPopup
item,
showProgress,
isSelected,
onClick,
onLongClick,
onDismissPopup
)
} else if (item is PlaylistInfoItem) {
PlaylistListItem(item, onClick)
@ -109,6 +123,7 @@ private fun determineItemViewMode(): ItemViewMode {
ItemViewMode.LIST
}
}
else -> viewMode
}
}

View File

@ -25,7 +25,7 @@ import org.schabi.newpipe.util.NO_SERVICE_ID
@Composable
fun PlaylistListItem(
playlist: PlaylistInfoItem,
onClick: (InfoItem) -> Unit = {},
onClick: (InfoItem) -> Unit = {}
) {
Row(
modifier = Modifier

View File

@ -77,7 +77,9 @@ fun StreamMenu(
onClick = {
onDismissRequest()
SparseItemUtil.fetchStreamInfoAndSaveToDatabase(
context, stream.serviceId, stream.url
context,
stream.serviceId,
stream.url
) { info ->
// TODO: Use an AlertDialog composable instead.
val downloadDialog = DownloadDialog(context, info)
@ -126,7 +128,10 @@ fun StreamMenu(
onClick = {
onDismissRequest()
SparseItemUtil.fetchUploaderUrlIfSparse(
context, stream.serviceId, stream.url, stream.uploaderUrl
context,
stream.serviceId,
stream.url,
stream.uploaderUrl
) { url ->
val activity = context.findFragmentActivity()
NavigationHelper.openChannelFragment(activity, stream, url)

View File

@ -23,14 +23,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.ImageStrategy
import org.schabi.newpipe.viewmodels.StreamViewModel
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Composable
fun StreamThumbnail(

View File

@ -4,12 +4,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NO_SERVICE_ID
import java.util.concurrent.TimeUnit
@Suppress("ktlint:standard:function-naming")
fun StreamInfoItem(
@ -64,6 +64,6 @@ internal class StreamItemPreviewProvider : PreviewParameterProvider<StreamInfoIt
override val values = sequenceOf(
StreamInfoItem(streamType = StreamType.NONE),
StreamInfoItem(streamType = StreamType.LIVE_STREAM),
StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM),
StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM)
)
}

View File

@ -52,7 +52,7 @@ fun RelatedItems(info: StreamInfo) {
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.auto_queue_description))
@ -95,7 +95,7 @@ private fun RelatedItemsPreview() {
info.relatedItems = listOf(
StreamInfoItem(streamType = StreamType.NONE),
StreamInfoItem(streamType = StreamType.LIVE_STREAM),
StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM),
StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM)
)
AppTheme {

View File

@ -66,7 +66,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
.animateContentSize()
.combinedClickable(
onLongClick = copyToClipboardCallback { parsedDescription },
onClick = { isExpanded = !isExpanded },
onClick = { isExpanded = !isExpanded }
)
.padding(start = 8.dp, top = 10.dp, end = 8.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -102,7 +102,9 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
Localization.concatenateStrings(
Localization.localizeUserName(comment.uploaderName),
Localization.relativeTimeOrTextual(
context, comment.uploadDate, comment.textualUploadDate
context,
comment.uploadDate,
comment.textualUploadDate
)
)
}
@ -110,7 +112,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
text = nameAndDate,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis
)
}
@ -127,7 +129,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -140,7 +142,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
contentDescription = stringResource(R.string.detail_likes_img_view_description),
modifier = Modifier
.padding(end = 4.dp)
.size(20.dp),
.size(20.dp)
)
Text(
text = Localization.likeCount(context, comment.likeCount),
@ -155,7 +157,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
imageVector = Icons.Default.Favorite,
contentDescription = stringResource(R.string.detail_heart_img_view_description),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
modifier = Modifier.size(20.dp)
)
}
}
@ -169,7 +171,9 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
modifier = Modifier.padding(end = 2.dp)
) {
val text = pluralStringResource(
R.plurals.replies, comment.replyCount, comment.replyCount.toString()
R.plurals.replies,
comment.replyCount,
comment.replyCount.toString()
)
Text(text = text)
}
@ -183,7 +187,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
CommentRepliesDialog(
parentComment = comment,
onDismissRequest = { showReplies = false },
onCommentAuthorOpened = onCommentAuthorOpened,
onCommentAuthorOpened = onCommentAuthorOpened
)
}
}
@ -200,7 +204,7 @@ fun CommentsInfoItem(
isHeartedByUploader: Boolean = false,
isPinned: Boolean = false,
replies: Page? = null,
replyCount: Int = 0,
replyCount: Int = 0
) = CommentsInfoItem(serviceId, url, name).apply {
this.commentText = commentText
this.uploaderName = uploaderName
@ -249,7 +253,7 @@ private class CommentPreviewProvider : CollectionPreviewParameterProvider<Commen
isHeartedByUploader = false,
replies = Page(""),
replyCount = 4283
),
)
)
)

View File

@ -15,7 +15,11 @@ class CommentInfo(
val isCommentsDisabled: Boolean
) {
constructor(commentsInfo: CommentsInfo) : this(
commentsInfo.serviceId, commentsInfo.url, commentsInfo.relatedItems, commentsInfo.nextPage,
commentsInfo.commentsCount, commentsInfo.isCommentsDisabled
commentsInfo.serviceId,
commentsInfo.url,
commentsInfo.relatedItems,
commentsInfo.nextPage,
commentsInfo.commentsCount,
commentsInfo.isCommentsDisabled
)
}

View File

@ -45,7 +45,7 @@ import org.schabi.newpipe.ui.theme.AppTheme
fun CommentRepliesDialog(
parentComment: CommentsInfoItem,
onDismissRequest: () -> Unit,
onCommentAuthorOpened: () -> Unit,
onCommentAuthorOpened: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val commentsFlow = remember {
@ -65,7 +65,7 @@ private fun CommentRepliesDialog(
parentComment: CommentsInfoItem,
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
onDismissRequest: () -> Unit,
onCommentAuthorOpened: () -> Unit,
onCommentAuthorOpened: () -> Unit
) {
val comments = commentsFlow.collectAsLazyPagingItems()
val nestedScrollInterop = rememberNestedScrollInteropConnection()
@ -83,7 +83,7 @@ private fun CommentRepliesDialog(
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismissRequest,
onDismissRequest = onDismissRequest
) {
LazyColumnThemedScrollbar(state = listState) {
LazyColumn(
@ -93,7 +93,7 @@ private fun CommentRepliesDialog(
item {
CommentRepliesHeader(
comment = parentComment,
onCommentAuthorOpened = nestedOnCommentAuthorOpened,
onCommentAuthorOpened = nestedOnCommentAuthorOpened
)
HorizontalDivider(
thickness = 1.dp,
@ -111,7 +111,7 @@ private fun CommentRepliesDialog(
text = pluralStringResource(
R.plurals.replies,
parentComment.replyCount,
parentComment.replyCount,
parentComment.replyCount
),
maxLines = 1,
style = MaterialTheme.typography.titleMedium
@ -125,6 +125,7 @@ private fun CommentRepliesDialog(
is LoadState.Loading -> {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
else -> {
// TODO use error panel instead
EmptyStateComposable(
@ -144,7 +145,7 @@ private fun CommentRepliesDialog(
items(comments.itemCount) {
Comment(
comment = comments[it]!!,
onCommentAuthorOpened = nestedOnCommentAuthorOpened,
onCommentAuthorOpened = nestedOnCommentAuthorOpened
)
}
}
@ -168,7 +169,7 @@ private fun CommentRepliesDialogPreview() {
CommentsInfoItem(
commentText = Description(
"Reply $i: ${LoremIpsum(i * i).values.first()}",
Description.PLAIN_TEXT,
Description.PLAIN_TEXT
),
uploaderName = LoremIpsum(11 - i).values.first()
)

View File

@ -44,7 +44,7 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@ -61,7 +61,7 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
}
.weight(1.0f, true),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
@ -78,11 +78,13 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
text = comment.uploaderName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall,
style = MaterialTheme.typography.titleSmall
)
Localization.relativeTimeOrTextual(
context, comment.uploadDate, comment.textualUploadDate
context,
comment.uploadDate,
comment.textualUploadDate
)?.let {
Text(text = it, style = MaterialTheme.typography.bodySmall)
}
@ -97,11 +99,11 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
if (comment.likeCount >= 0) {
Icon(
imageVector = Icons.Default.ThumbUp,
contentDescription = stringResource(R.string.detail_likes_img_view_description),
contentDescription = stringResource(R.string.detail_likes_img_view_description)
)
Text(
text = Localization.likeCount(context, comment.likeCount),
maxLines = 1,
maxLines = 1
)
}
@ -109,14 +111,14 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = stringResource(R.string.detail_heart_img_view_description),
tint = MaterialTheme.colorScheme.primary,
tint = MaterialTheme.colorScheme.primary
)
}
if (comment.isPinned) {
Icon(
imageVector = Icons.Default.PushPin,
contentDescription = stringResource(R.string.detail_pinned_comment_view_description),
contentDescription = stringResource(R.string.detail_pinned_comment_view_description)
)
}
}
@ -124,7 +126,7 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
DescriptionText(
description = comment.commentText,
style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodyMedium
)
}
}

View File

@ -111,6 +111,7 @@ private fun CommentSection(
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
}
is LoadState.Error -> {
val errorInfo = ErrorInfo(
throwable = refresh.error,
@ -131,6 +132,7 @@ private fun CommentSection(
}
}
}
else -> {
items(comments.itemCount) {
Comment(comment = comments[it]!!) {}
@ -139,6 +141,7 @@ private fun CommentSection(
}
}
}
is Resource.Error -> {
val errorInfo = ErrorInfo(
throwable = uiState.throwable,
@ -201,8 +204,12 @@ private fun CommentSectionSuccessPreview() {
CommentSection(
uiState = Resource.Success(
CommentInfo(
serviceId = 1, url = "", comments = comments, nextPage = null,
commentCount = 10, isCommentsDisabled = false
serviceId = 1,
url = "",
comments = comments,
nextPage = null,
commentCount = 10,
isCommentsDisabled = false
)
),
commentsFlow = flowOf(PagingData.from(comments))

View File

@ -22,17 +22,17 @@ import org.schabi.newpipe.ui.theme.AppTheme
@Composable
fun EmptyStateComposable(
spec: EmptyStateSpec,
modifier: Modifier = Modifier,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.Center
) {
Text(
text = spec.emojiText,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
textAlign = TextAlign.Center
)
Text(
@ -41,7 +41,7 @@ fun EmptyStateComposable(
.padding(horizontal = 16.dp),
text = stringResource(spec.descriptionText),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
textAlign = TextAlign.Center
)
}
}
@ -74,46 +74,46 @@ fun EmptyStateComposableNoCommentPreview() {
enum class EmptyStateSpec(
val emojiText: String,
@field:StringRes val descriptionText: Int,
@field:StringRes val descriptionText: Int
) {
GenericError(
emojiText = "¯\\_(ツ)_/¯",
descriptionText = R.string.empty_list_subtitle,
descriptionText = R.string.empty_list_subtitle
),
NoVideos(
emojiText = "(╯°-°)╯",
descriptionText = R.string.no_videos,
descriptionText = R.string.no_videos
),
NoComments(
emojiText = "¯\\_(╹x╹)_/¯",
descriptionText = R.string.no_comments,
descriptionText = R.string.no_comments
),
DisabledComments(
emojiText = "¯\\_(╹x╹)_/¯",
descriptionText = R.string.comments_are_disabled,
descriptionText = R.string.comments_are_disabled
),
ErrorLoadingComments(
emojiText = "¯\\_(╹x╹)_/¯",
descriptionText = R.string.error_unable_to_load_comments,
descriptionText = R.string.error_unable_to_load_comments
),
NoSearchResult(
emojiText = "╰(°●°╰)",
descriptionText = R.string.search_no_results,
descriptionText = R.string.search_no_results
),
ContentNotSupported(
emojiText = "(︶︹︺)",
descriptionText = R.string.content_not_supported,
descriptionText = R.string.content_not_supported
),
NoBookmarkedPlaylist(
emojiText = "(╥﹏╥)",
descriptionText = R.string.no_playlist_bookmarked_yet,
descriptionText = R.string.no_playlist_bookmarked_yet
),
NoSubscriptionsHint(
emojiText = "(꩜ᯅ꩜)",
descriptionText = R.string.import_subscriptions_hint,
descriptionText = R.string.import_subscriptions_hint
),
NoSubscriptions(
emojiText = "(꩜ᯅ꩜)",
descriptionText = R.string.no_channel_subscribed_yet,
),
descriptionText = R.string.no_channel_subscribed_yet
)
}

View File

@ -17,7 +17,7 @@ import org.schabi.newpipe.ui.theme.AppTheme
@JvmOverloads
fun ComposeView.setEmptyStateComposable(
spec: EmptyStateSpec = EmptyStateSpec.GenericError,
strategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
strategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
) = apply {
setViewCompositionStrategy(strategy)
setContent {

View File

@ -44,7 +44,7 @@ private val lightScheme = lightColorScheme(
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
surfaceContainerHighest = surfaceContainerHighestLight
)
private val darkScheme = darkColorScheme(
@ -82,7 +82,7 @@ private val darkScheme = darkColorScheme(
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
surfaceContainerHighest = surfaceContainerHighestDark
)
private val blackScheme = darkScheme.copy(surface = Color.Black)

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.sync.withLock
* */
class GuardedByMutex<T>(
private var data: T,
private val lock: Mutex = Mutex(locked = false),
private val lock: Mutex = Mutex(locked = false)
) {
/** Lock the mutex and access the data, blocking the current thread.
@ -18,20 +18,18 @@ class GuardedByMutex<T>(
* */
fun <Y> runWithLockSync(
action: MutexData<T>.() -> Y
) =
runBlocking {
lock.withLock {
MutexData(data, { d -> data = d }).action()
}
) = runBlocking {
lock.withLock {
MutexData(data, { d -> data = d }).action()
}
}
/** Lock the mutex and access the data, suspending the coroutine.
* @param action to run with locked mutex
* */
suspend fun <Y> runWithLock(action: MutexData<T>.() -> Y) =
lock.withLock {
MutexData(data, { d -> data = d }).action()
}
suspend fun <Y> runWithLock(action: MutexData<T>.() -> Y) = lock.withLock {
MutexData(data, { d -> data = d }).action()
}
}
/** The data inside a [GuardedByMutex], which can be accessed via [lockData].

View File

@ -17,11 +17,11 @@ import coil3.size.Size
import coil3.target.Target
import coil3.toBitmap
import coil3.transform.Transformation
import kotlin.math.min
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.ktx.scale
import kotlin.math.min
object CoilHelper {
private val TAG = CoilHelper::class.java.simpleName
@ -30,37 +30,36 @@ 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? = context.imageLoader
.executeBlocking(getImageRequest(context, url, placeholderResId).build())
.image
?.toBitmap()
fun loadAvatar(
target: ImageView,
images: List<Image>,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_person)
}
fun loadAvatar(
target: ImageView,
url: String?,
url: String?
) {
loadImageDefault(target, url, R.drawable.placeholder_person)
}
fun loadThumbnail(
target: ImageView,
images: List<Image>,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video)
}
fun loadThumbnail(
target: ImageView,
url: String?,
url: String?
) {
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video)
}
@ -68,7 +67,7 @@ object CoilHelper {
fun loadScaledDownThumbnail(
context: Context,
images: List<Image>,
target: Target,
target: Target
): Disposable {
val url = ImageStrategy.choosePreferredImage(images)
val request =
@ -80,7 +79,7 @@ object CoilHelper {
override suspend fun transform(
input: Bitmap,
size: Size,
size: Size
): Bitmap {
if (MainActivity.DEBUG) {
Log.d(TAG, "Thumbnail - transform() called")
@ -89,7 +88,7 @@ object CoilHelper {
val notificationThumbnailWidth =
min(
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
input.width.toFloat(),
input.width.toFloat()
).toInt()
var newHeight = input.height / (input.width / notificationThumbnailWidth)
@ -104,7 +103,7 @@ object CoilHelper {
result
}
}
},
}
).build()
return context.imageLoader.enqueue(request)
@ -112,7 +111,7 @@ object CoilHelper {
fun loadDetailsThumbnail(
target: ImageView,
images: List<Image>,
images: List<Image>
) {
val url = ImageStrategy.choosePreferredImage(images)
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false)
@ -120,21 +119,21 @@ object CoilHelper {
fun loadBanner(
target: ImageView,
images: List<Image>,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_channel_banner)
}
fun loadPlaylistThumbnail(
target: ImageView,
images: List<Image>,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist)
}
fun loadPlaylistThumbnail(
target: ImageView,
url: String?,
url: String?
) {
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist)
}
@ -142,7 +141,7 @@ object CoilHelper {
private fun loadImageDefault(
target: ImageView,
images: List<Image>,
@DrawableRes placeholderResId: Int,
@DrawableRes placeholderResId: Int
) {
loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId)
}
@ -151,7 +150,7 @@ object CoilHelper {
target: ImageView,
url: String?,
@DrawableRes placeholderResId: Int,
showPlaceholder: Boolean = true,
showPlaceholder: Boolean = true
) {
val request =
getImageRequest(target.context, url, placeholderResId, showPlaceholder)
@ -164,7 +163,7 @@ 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

View File

@ -23,11 +23,11 @@ val NewPipeSquircleIcon: ImageVector
defaultWidth = 100.0.dp,
defaultHeight = 100.0.dp,
viewportWidth = 100.0f,
viewportHeight = 100.0f,
viewportHeight = 100.0f
).apply {
// M0 50 C0 15 15 0 50 0 s50 15 50 50 -15 50 -50 50 S0 85 0 50
path(
fill = SolidColor(Color(0xFFCD201F)),
fill = SolidColor(Color(0xFFCD201F))
) {
// M 0 50
moveTo(x = 0.0f, y = 50.0f)
@ -38,33 +38,33 @@ val NewPipeSquircleIcon: ImageVector
x2 = 15.0f,
y2 = 0.0f,
x3 = 50.0f,
y3 = 0.0f,
y3 = 0.0f
)
// s 50 15 50 50
reflectiveCurveToRelative(
dx1 = 50.0f,
dy1 = 15.0f,
dx2 = 50.0f,
dy2 = 50.0f,
dy2 = 50.0f
)
// s -15 50 -50 50
reflectiveCurveToRelative(
dx1 = -15.0f,
dy1 = 50.0f,
dx2 = -50.0f,
dy2 = 50.0f,
dy2 = 50.0f
)
// S 0 85 0 50
reflectiveCurveTo(
x1 = 0.0f,
y1 = 85.0f,
x2 = 0.0f,
y2 = 50.0f,
y2 = 50.0f
)
}
// M31.7 19.2 v61.7 l9.7 -5.73 V36 l23.8 14 -17.6 10.35 V71.5 L84 50
path(
fill = SolidColor(Color(0xFFFFFFFF)),
fill = SolidColor(Color(0xFFFFFFFF))
) {
// M 31.7 19.2
moveTo(x = 31.7f, y = 19.2f)
@ -91,7 +91,7 @@ val NewPipeSquircleIcon: ImageVector
private fun IconPreview() {
Image(
imageVector = NewPipeSquircleIcon,
contentDescription = null,
contentDescription = null
)
}

View File

@ -1,6 +1,11 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
import java.io.File
import java.io.IOException
import kotlin.io.path.createTempFile
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import org.junit.Assert
import org.junit.Test
import org.mockito.Mockito
@ -8,11 +13,6 @@ import org.schabi.newpipe.settings.export.BackupFileLocator
import org.schabi.newpipe.settings.export.ImportExportManager
import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream
import java.io.File
import java.io.IOException
import kotlin.io.path.createTempFile
import kotlin.io.path.exists
import kotlin.io.path.fileSize
class ImportAllCombinationsTest {
@ -23,7 +23,7 @@ class ImportAllCombinationsTest {
private enum class Ser(val id: String) {
YES("ser"),
VULNERABLE("vulnser"),
NO("noser");
NO("noser")
}
private data class FailData(
@ -31,7 +31,7 @@ class ImportAllCombinationsTest {
val containsSer: Ser,
val containsJson: Boolean,
val filename: String,
val throwable: Throwable,
val throwable: Throwable
)
private fun testZipCombination(
@ -39,7 +39,7 @@ class ImportAllCombinationsTest {
containsSer: Ser,
containsJson: Boolean,
filename: String,
runTest: (test: () -> Unit) -> Unit,
runTest: (test: () -> Unit) -> Unit
) {
val zipFile = File(classloader.getResource(filename)?.file!!)
val zip = Mockito.mock(StoredFileHelper::class.java, Mockito.withSettings().stubOnly())
@ -95,6 +95,7 @@ class ImportAllCombinationsTest {
Mockito.verify(editor, Mockito.atLeastOnce())
.putInt(Mockito.anyString(), Mockito.anyInt())
}
Ser.VULNERABLE -> runTest {
Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
Assert.assertThrows(ClassNotFoundException::class.java) {
@ -104,6 +105,7 @@ class ImportAllCombinationsTest {
Mockito.verify(editor, Mockito.never()).clear()
Mockito.verify(editor, Mockito.never()).commit()
}
Ser.NO -> runTest {
Assert.assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
Assert.assertThrows(IOException::class.java) {
@ -154,15 +156,18 @@ class ImportAllCombinationsTest {
for (containsSer in Ser.entries) {
for (containsJson in listOf(true, false)) {
val filename = "settings/${if (containsDb) "db" else "nodb"}_${
containsSer.id}_${if (containsJson) "json" else "nojson"}.zip"
containsSer.id}_${if (containsJson) "json" else "nojson"}.zip"
testZipCombination(containsDb, containsSer, containsJson, filename) { test ->
try {
test()
} catch (e: Throwable) {
failedAssertions.add(
FailData(
containsDb, containsSer, containsJson,
filename, e
containsDb,
containsSer,
containsJson,
filename,
e
)
)
}
@ -175,7 +180,7 @@ class ImportAllCombinationsTest {
for (a in failedAssertions) {
println(
"Assertion failed with containsDb=${a.containsDb}, containsSer=${
a.containsSer}, containsJson=${a.containsJson}, filename=${a.filename}:"
a.containsSer}, containsJson=${a.containsJson}, filename=${a.filename}:"
)
a.throwable.printStackTrace()
println()

View File

@ -4,8 +4,15 @@ import android.content.SharedPreferences
import com.grack.nanojson.JsonParser
import java.io.File
import java.io.ObjectInputStream
import java.nio.file.Files
import java.nio.file.Paths
import java.util.zip.ZipFile
import kotlin.io.path.createTempDirectory
import kotlin.io.path.createTempFile
import kotlin.io.path.deleteIfExists
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.inputStream
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
@ -27,20 +34,6 @@ import org.schabi.newpipe.settings.export.BackupFileLocator
import org.schabi.newpipe.settings.export.ImportExportManager
import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream
<<<<<<< HEAD
import java.io.File
import java.io.ObjectInputStream
import java.nio.file.Paths
import java.util.zip.ZipFile
import kotlin.io.path.createTempDirectory
import kotlin.io.path.createTempFile
import kotlin.io.path.deleteIfExists
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.inputStream
=======
>>>>>>> dev
@RunWith(MockitoJUnitRunner::class)
class ImportExportManagerTest {