Fix all ktlint violations and merge issues
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
parent
394a7f68cd
commit
2376a83e0c
@ -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}.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()}]"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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?
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
)
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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].
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user