diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8e5b4d0ec..f7b10d0ac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: with: fetch-depth: 1 - name: ktlint - uses: ScaCap/action-ktlint@1.3 + uses: ScaCap/action-ktlint@1.4 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-check diff --git a/app/src/main/java/com/github/libretube/BackgroundMode.kt b/app/src/main/java/com/github/libretube/BackgroundMode.kt index c85e4fd06..3f51ea58b 100644 --- a/app/src/main/java/com/github/libretube/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/BackgroundMode.kt @@ -3,6 +3,7 @@ package com.github.libretube import android.content.Context import android.support.v4.media.session.MediaSessionCompat import com.github.libretube.obj.Streams +import com.github.libretube.util.DescriptionAdapter import com.github.libretube.util.RetrofitInstance import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlayer @@ -10,6 +11,7 @@ import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ui.PlayerNotificationManager +import gen._base._base_java__rjava_resources.srcjar.R.id.title import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -66,13 +68,20 @@ class BackgroundMode { /** * Initializes the [playerNotification] attached to the [player] and shows it. */ - private fun initializePlayerNotification(c: Context) { + private fun initializePlayerNotification(c: Context, streams: Streams) { playerNotification = PlayerNotificationManager - .Builder(c, 1, "background_mode").build() - playerNotification.setPlayer(player) - playerNotification.setUsePreviousAction(false) - playerNotification.setUseNextAction(false) - playerNotification.setMediaSessionToken(mediaSession.sessionToken) + .Builder(c, 1, "background_mode") + // set the description of the notification + .setMediaDescriptionAdapter( + DescriptionAdapter(streams.title!!, streams.uploader!!, streams.thumbnailUrl!!) + ) + .build() + playerNotification.apply { + setPlayer(player) + setUsePreviousAction(false) + setUseNextAction(false) + setMediaSessionToken(mediaSession.sessionToken) + } } /** @@ -104,7 +113,7 @@ class BackgroundMode { job.join() initializePlayer(c) - initializePlayerNotification(c) + initializePlayerNotification(c, response!!) player?.apply { playWhenReady = playWhenReadyPlayer diff --git a/app/src/main/java/com/github/libretube/MainActivity.kt b/app/src/main/java/com/github/libretube/MainActivity.kt index 183fe853f..db9894118 100644 --- a/app/src/main/java/com/github/libretube/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/MainActivity.kt @@ -10,6 +10,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler +import android.os.Looper import android.util.Log import android.util.TypedValue import android.view.View @@ -68,6 +69,12 @@ class MainActivity : AppCompatActivity() { sharedPreferences.getBoolean("sponsors_category_key", false) SponsorBlockSettings.outroEnabled = sharedPreferences.getBoolean("outro_category_key", false) + SponsorBlockSettings.fillerEnabled = + sharedPreferences.getBoolean("filler_category_key", false) + SponsorBlockSettings.musicOfftopicEnabled = + sharedPreferences.getBoolean("music_offtopic_category_key", false) + SponsorBlockSettings.previewEnabled = + sharedPreferences.getBoolean("preview_category_key", false) ThemeHelper().updateTheme(this) LocaleHelper().updateLanguage(this) @@ -199,19 +206,12 @@ class MainActivity : AppCompatActivity() { .replace("/embed/", "") val bundle = Bundle() bundle.putString("videoId", watch) - val frag = PlayerFragment() - frag.arguments = bundle - supportFragmentManager.beginTransaction() - .remove(PlayerFragment()) - .commit() - supportFragmentManager.beginTransaction() - .replace(R.id.container, frag) - .commitNow() - Handler().postDelayed({ - val motionLayout = findViewById(R.id.playerMotionLayout) - motionLayout.transitionToEnd() - motionLayout.transitionToStart() - }, 100) + // for time stamped links + if (data.query != null && data.query?.contains("t=")!!) { + val timeStamp = data.query.toString().split("t=")[1] + bundle.putLong("timeStamp", timeStamp.toLong()) + } + loadWatch(bundle) } else if (data.path!!.contains("/watch") && data.query != null) { Log.d("dafaq", data.query!!) var watch = data.query!! @@ -226,39 +226,41 @@ class MainActivity : AppCompatActivity() { } var bundle = Bundle() bundle.putString("videoId", watch.replace("v=", "")) - var frag = PlayerFragment() - frag.arguments = bundle - supportFragmentManager.beginTransaction() - .remove(PlayerFragment()) - .commit() - supportFragmentManager.beginTransaction() - .replace(R.id.container, frag) - .commitNow() - Handler().postDelayed({ - val motionLayout = findViewById(R.id.playerMotionLayout) - motionLayout.transitionToEnd() - motionLayout.transitionToStart() - }, 100) + // for time stamped links + if (data.query != null && data.query?.contains("t=")!!) { + val timeStamp = data.query.toString().split("t=")[1] + bundle.putLong("timeStamp", timeStamp.toLong()) + } + loadWatch(bundle) } else { var watch = data.path!!.replace("/", "") var bundle = Bundle() bundle.putString("videoId", watch) - var frag = PlayerFragment() - frag.arguments = bundle - supportFragmentManager.beginTransaction() - .remove(PlayerFragment()) - .commit() - supportFragmentManager.beginTransaction() - .replace(R.id.container, frag) - .commitNow() - Handler().postDelayed({ - val motionLayout = findViewById(R.id.playerMotionLayout) - motionLayout.transitionToEnd() - motionLayout.transitionToStart() - }, 100) + // for time stamped links + if (data.query != null && data.query?.contains("t=")!!) { + val timeStamp = data.query.toString().split("t=")[1] + bundle.putLong("timeStamp", timeStamp.toLong()) + } + loadWatch(bundle) } } + private fun loadWatch(bundle: Bundle) { + var frag = PlayerFragment() + frag.arguments = bundle + supportFragmentManager.beginTransaction() + .remove(PlayerFragment()) + .commit() + supportFragmentManager.beginTransaction() + .replace(R.id.container, frag) + .commitNow() + Handler(Looper.getMainLooper()).postDelayed({ + val motionLayout = findViewById(R.id.playerMotionLayout) + motionLayout.transitionToEnd() + motionLayout.transitionToStart() + }, 100) + } + override fun onBackPressed() { try { val mainMotionLayout = findViewById(R.id.mainMotionLayout) diff --git a/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt b/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt index 28f716ac1..7dbf5d9d7 100644 --- a/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt @@ -8,14 +8,19 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView import com.github.libretube.R +import com.github.libretube.dialogs.VideoOptionsDialog import com.github.libretube.fragments.PlayerFragment import com.github.libretube.obj.StreamItem import com.github.libretube.util.formatShort import com.squareup.picasso.Picasso -class ChannelAdapter(private val videoFeed: MutableList) : +class ChannelAdapter( + private val videoFeed: MutableList, + private val childFragmentManager: FragmentManager +) : RecyclerView.Adapter() { override fun getItemCount(): Int { return videoFeed.size @@ -55,6 +60,12 @@ class ChannelAdapter(private val videoFeed: MutableList) : .replace(R.id.container, frag) .commitNow() } + holder.v.setOnLongClickListener { + val videoId = trending.url!!.replace("/watch?v=", "") + VideoOptionsDialog(videoId, holder.v.context) + .show(childFragmentManager, VideoOptionsDialog.TAG) + true + } } } diff --git a/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt b/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt new file mode 100644 index 000000000..66a0d7247 --- /dev/null +++ b/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt @@ -0,0 +1,48 @@ +package com.github.libretube.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.github.libretube.R +import com.github.libretube.obj.ChapterSegment +import com.google.android.exoplayer2.ExoPlayer +import com.squareup.picasso.Picasso + +class ChaptersAdapter( + private val chapters: List, + private val exoPlayer: ExoPlayer +) : RecyclerView.Adapter() { + val TAG = "ChaptersAdapter" + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val cell = layoutInflater.inflate(R.layout.chapter_column, parent, false) + return ChaptersViewHolder(cell) + } + + override fun onBindViewHolder(holder: ChaptersViewHolder, position: Int) { + val chapter = chapters[position] + val chapterImage = holder.v.findViewById(R.id.chapter_image) + Picasso.get().load(chapter.image).fit().centerCrop().into(chapterImage) + + val chapterTitle = holder.v.findViewById(R.id.chapter_title) + chapterTitle.text = chapter.title + + holder.v.setOnClickListener { + val chapterStart = chapter.start!!.toLong() * 1000 // multiply by thousand for ms -> s + exoPlayer.seekTo(chapterStart) + } + } + + override fun getItemCount(): Int { + return chapters.size + } +} + +class ChaptersViewHolder(val v: View) : RecyclerView.ViewHolder(v) { + init { + } +} diff --git a/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt b/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt index 19a521ee8..532808e0d 100644 --- a/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt +++ b/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt @@ -67,7 +67,8 @@ class VideoOptionsDialog(private val videoId: String, context: Context) : Dialog } 2 -> { val shareDialog = ShareDialog(videoId) - shareDialog.show(childFragmentManager, "ShareDialog") + // using parentFragmentManager is important here + shareDialog.show(parentFragmentManager, "ShareDialog") } else -> { dialog.dismiss() diff --git a/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt index d7c3fd030..f42f95b69 100644 --- a/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt @@ -218,7 +218,10 @@ class ChannelFragment : Fragment() { val channelImage = view.findViewById(R.id.channel_image) Picasso.get().load(response.bannerUrl).into(bannerImage) Picasso.get().load(response.avatarUrl).into(channelImage) - channelAdapter = ChannelAdapter(response.relatedStreams!!.toMutableList()) + channelAdapter = ChannelAdapter( + response.relatedStreams!!.toMutableList(), + childFragmentManager + ) view.findViewById(R.id.channel_recView).adapter = channelAdapter } } diff --git a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt index cfec34244..28d552abb 100644 --- a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt @@ -3,7 +3,6 @@ package com.github.libretube.fragments import android.annotation.SuppressLint import android.app.NotificationManager import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Rect @@ -18,9 +17,6 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.Animation -import android.view.animation.LinearInterpolator -import android.view.animation.RotateAnimation import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ImageView @@ -43,6 +39,7 @@ import androidx.recyclerview.widget.RecyclerView import com.github.libretube.IS_DOWNLOAD_RUNNING import com.github.libretube.MainActivity import com.github.libretube.R +import com.github.libretube.adapters.ChaptersAdapter import com.github.libretube.adapters.CommentsAdapter import com.github.libretube.adapters.TrendingAdapter import com.github.libretube.dialogs.AddtoPlaylistDialog @@ -57,6 +54,7 @@ import com.github.libretube.obj.Streams import com.github.libretube.obj.Subscribe import com.github.libretube.preferences.SponsorBlockSettings import com.github.libretube.util.CronetHelper +import com.github.libretube.util.DescriptionAdapter import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.formatShort import com.google.android.exoplayer2.C @@ -102,7 +100,7 @@ class PlayerFragment : Fragment() { private var whichQuality = 0 private var isZoomed: Boolean = false - var isSubscribed: Boolean = false + private var isSubscribed: Boolean = false private lateinit var relatedRecView: RecyclerView private lateinit var commentsRecView: RecyclerView @@ -122,6 +120,10 @@ class PlayerFragment : Fragment() { private lateinit var mediaSessionConnector: MediaSessionConnector private lateinit var playerNotification: PlayerNotificationManager + private lateinit var title: String + private lateinit var uploader: String + private lateinit var thumbnailUrl: String + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { @@ -142,6 +144,11 @@ class PlayerFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + initializeTransitionLayout(view) + fetchJsonAndInitPlayer(view) + } + + private fun initializeTransitionLayout(view: View) { val playerDescription = view.findViewById(R.id.player_description) videoId = videoId!!.replace("/watch?v=", "") relDownloadVideo = view.findViewById(R.id.relPlayer_download) @@ -208,7 +215,7 @@ class PlayerFragment : Fragment() { playerMotionLayout.progress = 1.toFloat() playerMotionLayout.transitionToStart() - fetchJson(view) + view.findViewById(R.id.close_imageView).setOnClickListener { motionLayout.transitionToEnd() val mainActivity = activity as MainActivity @@ -239,25 +246,11 @@ class PlayerFragment : Fragment() { } view.findViewById(R.id.player_title_layout).setOnClickListener { + val image = view.findViewById(R.id.player_description_arrow) + image.animate().rotationBy(180F).setDuration(100).start() if (playerDescription.isVisible) { - val image = view.findViewById(R.id.player_description_arrow) - image.clearAnimation() playerDescription.visibility = View.GONE } else { - // toggle button - val rotate = RotateAnimation( - 0F, - 180F, - Animation.RELATIVE_TO_SELF, - 0.5f, - Animation.RELATIVE_TO_SELF, - 0.5f - ) - rotate.duration = 100 - rotate.interpolator = LinearInterpolator() - rotate.fillAfter = true - val image = view.findViewById(R.id.player_description_arrow) - image.startAnimation(rotate) playerDescription.visibility = View.VISIBLE } } @@ -319,8 +312,6 @@ class PlayerFragment : Fragment() { commentsRecView.layoutManager = LinearLayoutManager(view.context) commentsRecView.setItemViewCacheSize(20) - commentsRecView.isDrawingCacheEnabled = true - commentsRecView.drawingCacheQuality = View.DRAWING_CACHE_QUALITY_HIGH relatedRecView = view.findViewById(R.id.player_recView) relatedRecView.layoutManager = @@ -372,7 +363,7 @@ class PlayerFragment : Fragment() { } } - private fun fetchJson(view: View) { + private fun fetchJsonAndInitPlayer(view: View) { fun run() { lifecycleScope.launchWhenCreated { val response = try { @@ -387,6 +378,40 @@ class PlayerFragment : Fragment() { Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show() return@launchWhenCreated } + // for the notification description adapter + title = response.title!! + uploader = response.uploader!! + thumbnailUrl = response.thumbnailUrl!! + + // check whether related streams are enabled + val sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(requireContext()) + relatedStreamsEnabled = sharedPreferences.getBoolean("related_streams_toggle", true) + runOnUiThread { + createExoPlayer(view) + prepareExoPlayerView() + if (response.chapters != null) initializeChapters(response.chapters) + setResolutionAndSubtitles(view, response) + // support for time stamped links + if (arguments?.getLong("timeStamp") != null) { + val position = arguments?.getLong("timeStamp")!! * 1000 + exoPlayer.seekTo(position) + } + exoPlayer.prepare() + exoPlayer.play() + initializePlayerView(view, response) + initializePlayerNotification(requireContext()) + fetchSponsorBlockSegments() + if (!relatedStreamsEnabled) toggleComments() + } + } + } + run() + } + + private fun fetchSponsorBlockSegments() { + fun run() { + lifecycleScope.launchWhenCreated { if (SponsorBlockSettings.sponsorBlockEnabled) { val categories: ArrayList = arrayListOf() if (SponsorBlockSettings.introEnabled) { @@ -404,6 +429,15 @@ class PlayerFragment : Fragment() { if (SponsorBlockSettings.outroEnabled) { categories.add("outro") } + if (SponsorBlockSettings.fillerEnabled) { + categories.add("filler") + } + if (SponsorBlockSettings.musicOfftopicEnabled) { + categories.add("music_offtopic") + } + if (SponsorBlockSettings.previewEnabled) { + categories.add("preview") + } if (categories.size > 0) { segmentData = try { @@ -425,20 +459,6 @@ class PlayerFragment : Fragment() { } } } - // check whether related streams are enabled - val sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(requireContext()) - relatedStreamsEnabled = sharedPreferences.getBoolean("related_streams_toggle", true) - runOnUiThread { - createExoPlayer(view) - prepareExoPlayerView() - if (response.chapters != null) initializeChapters(response.chapters) - setResolutionAndSubtitles(view, response) - exoPlayer.prepare() - exoPlayer.play() - initializePlayerView(view, response) - if (!relatedStreamsEnabled) toggleComments() - } } } run() @@ -470,7 +490,7 @@ class PlayerFragment : Fragment() { view.findViewById(R.id.player_description).text = response.description // Listener for play and pause icon change - exoPlayer.addListener(object : com.google.android.exoplayer2.Player.Listener { + exoPlayer.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { if (isPlaying && SponsorBlockSettings.sponsorBlockEnabled) { exoPlayerView.postDelayed( @@ -479,7 +499,7 @@ class PlayerFragment : Fragment() { ) } } - + @Deprecated(message = "Deprecated", level = DeprecationLevel.HIDDEN) override fun onPlayerStateChanged( playWhenReady: Boolean, playbackState: Int @@ -520,7 +540,7 @@ class PlayerFragment : Fragment() { relDownloadVideo.setOnClickListener { if (!IS_DOWNLOAD_RUNNING) { val newFragment = DownloadDialog() - var bundle = Bundle() + val bundle = Bundle() bundle.putString("video_id", videoId) bundle.putParcelable("streams", response) newFragment.arguments = bundle @@ -588,7 +608,7 @@ class PlayerFragment : Fragment() { isSubscribed(subButton, channelId!!) view.findViewById(R.id.save).setOnClickListener { val newFragment = AddtoPlaylistDialog() - var bundle = Bundle() + val bundle = Bundle() bundle.putString("videoId", videoId) newFragment.arguments = bundle newFragment.show(childFragmentManager, "AddToPlaylist") @@ -597,8 +617,28 @@ class PlayerFragment : Fragment() { } private fun initializeChapters(chapters: List) { - chapters.forEach { chapter -> - Log.e(TAG, chapter.title!!) + val chaptersToggle = view?.findViewById(R.id.chapters_toggle) + val chaptersRecView = view?.findViewById(R.id.chapters_recView) + val chaptersToggleText = view?.findViewById(R.id.chapters_toggle_text) + val chaptersToggleArrow = view?.findViewById(R.id.chapters_toggle_arrow) + + if (chapters.isNotEmpty()) { + chaptersToggle?.visibility = View.VISIBLE + + chaptersToggle?.setOnClickListener { + if (chaptersRecView?.isVisible!!) { + chaptersRecView?.visibility = View.GONE + chaptersToggleText?.text = getString(R.string.show_chapters) + } else { + chaptersRecView?.visibility = View.VISIBLE + chaptersToggleText?.text = getString(R.string.hide_chapters) + } + chaptersToggleArrow!!.animate().setDuration(100).rotationBy(180F).start() + } + + chaptersRecView?.layoutManager = + LinearLayoutManager(this.context, LinearLayoutManager.HORIZONTAL, false) + chaptersRecView?.adapter = ChaptersAdapter(chapters, exoPlayer) } } @@ -609,7 +649,7 @@ class PlayerFragment : Fragment() { val name = vid.quality + " " + vid.format videosNameArray += name } - var subtitle = mutableListOf() + val subtitle = mutableListOf() if (response.subtitles!!.isNotEmpty()) { subtitle.add( SubtitleConfiguration.Builder(response.subtitles[0].url!!.toUri()) @@ -719,70 +759,69 @@ class PlayerFragment : Fragment() { val builder: MaterialAlertDialogBuilder? = activity?.let { MaterialAlertDialogBuilder(it) } - var lastPosition = exoPlayer.currentPosition + val lastPosition = exoPlayer.currentPosition builder!!.setTitle(R.string.choose_quality_dialog) .setItems( - videosNameArray, - DialogInterface.OnClickListener { _, which -> - whichQuality = which - if (response.subtitles.isNotEmpty()) { - var subtitle = - mutableListOf() - subtitle.add( - SubtitleConfiguration.Builder( - response.subtitles[0].url!!.toUri() - ) - .setMimeType(response.subtitles[0].mimeType!!) // The correct MIME type (required). - .setLanguage(response.subtitles[0].code) // The subtitle language (optional). - .build() + videosNameArray + ) { _, which -> + whichQuality = which + if (response.subtitles.isNotEmpty()) { + val subtitle = + mutableListOf() + subtitle.add( + SubtitleConfiguration.Builder( + response.subtitles[0].url!!.toUri() ) - } - if (which == 0) { - val mediaItem: MediaItem = MediaItem.Builder() - .setUri(response.hls) - .setSubtitleConfigurations(subtitle) + .setMimeType(response.subtitles[0].mimeType!!) // The correct MIME type (required). + .setLanguage(response.subtitles[0].code) // The subtitle language (optional). .build() - exoPlayer.setMediaItem(mediaItem) - } else { - val dataSourceFactory: DataSource.Factory = - DefaultHttpDataSource.Factory() - val videoItem: MediaItem = MediaItem.Builder() - .setUri(response.videoStreams[which - 1].url) - .setSubtitleConfigurations(subtitle) - .build() - val videoSource: MediaSource = - DefaultMediaSourceFactory(dataSourceFactory) - .createMediaSource(videoItem) - var audioSource: MediaSource = - DefaultMediaSourceFactory(dataSourceFactory) - .createMediaSource( - fromUri(response.audioStreams!![0].url!!) - ) - if (response.videoStreams[which - 1].quality == "720p" || - response.videoStreams[which - 1].quality == "1080p" || - response.videoStreams[which - 1].quality == "480p" - ) { - audioSource = - ProgressiveMediaSource.Factory(dataSourceFactory) - .createMediaSource( - fromUri( - response.audioStreams[ - getMostBitRate( - response.audioStreams - ) - ].url!! - ) - ) - } - val mergeSource: MediaSource = - MergingMediaSource(videoSource, audioSource) - exoPlayer.setMediaSource(mergeSource) - } - exoPlayer.seekTo(lastPosition) - view.findViewById(R.id.quality_text).text = - videosNameArray[which] + ) } - ) + if (which == 0) { + val mediaItem: MediaItem = MediaItem.Builder() + .setUri(response.hls) + .setSubtitleConfigurations(subtitle) + .build() + exoPlayer.setMediaItem(mediaItem) + } else { + val dataSourceFactory: DataSource.Factory = + DefaultHttpDataSource.Factory() + val videoItem: MediaItem = MediaItem.Builder() + .setUri(response.videoStreams[which - 1].url) + .setSubtitleConfigurations(subtitle) + .build() + val videoSource: MediaSource = + DefaultMediaSourceFactory(dataSourceFactory) + .createMediaSource(videoItem) + var audioSource: MediaSource = + DefaultMediaSourceFactory(dataSourceFactory) + .createMediaSource( + fromUri(response.audioStreams!![0].url!!) + ) + if (response.videoStreams[which - 1].quality == "720p" || + response.videoStreams[which - 1].quality == "1080p" || + response.videoStreams[which - 1].quality == "480p" + ) { + audioSource = + ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource( + fromUri( + response.audioStreams[ + getMostBitRate( + response.audioStreams + ) + ].url!! + ) + ) + } + val mergeSource: MediaSource = + MergingMediaSource(videoSource, audioSource) + exoPlayer.setMediaSource(mergeSource) + } + exoPlayer.seekTo(lastPosition) + view.findViewById(R.id.quality_text).text = + videosNameArray[which] + } val dialog = builder.create() dialog.show() } @@ -822,8 +861,6 @@ class PlayerFragment : Fragment() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val playbackSpeed = sharedPreferences.getString("playback_speed", "1F")?.toFloat() exoPlayer.setPlaybackSpeed(playbackSpeed!!) - - initializePlayerNotification(requireContext()) } private fun initializePlayerNotification(c: Context) { @@ -838,6 +875,9 @@ class PlayerFragment : Fragment() { playerNotification = PlayerNotificationManager .Builder(c, 1, "background_mode") + .setMediaDescriptionAdapter( + DescriptionAdapter(title, uploader, thumbnailUrl) + ) .build() playerNotification.apply { diff --git a/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt b/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt index 63ddd43e4..a61714dd8 100644 --- a/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt @@ -198,7 +198,6 @@ class SearchFragment : Fragment() { lifecycleScope.launchWhenCreated { isFetchingSearch = true hideKeyboard() - Log.e("here", "here") val response = try { RetrofitInstance.api.getSearchResults(query, apiSearchFilter) } catch (e: IOException) { diff --git a/app/src/main/java/com/github/libretube/preferences/AboutFragment.kt b/app/src/main/java/com/github/libretube/preferences/AboutFragment.kt index d7de5f47d..851c6a12a 100644 --- a/app/src/main/java/com/github/libretube/preferences/AboutFragment.kt +++ b/app/src/main/java/com/github/libretube/preferences/AboutFragment.kt @@ -29,40 +29,42 @@ class AboutFragment : Fragment() { val topBarText = activity?.findViewById(R.id.topBar_textView) topBarText?.text = getString(R.string.about) - val appVersion = view?.findViewById(R.id.app_version) + val appVersion = view.findViewById(R.id.app_version) appVersion.text = BuildConfig.VERSION_NAME - val website = view?.findViewById(R.id.website) - website?.setOnClickListener { + val website = view.findViewById(R.id.website) + website.setOnClickListener { openLinkFromHref("https://libre-tube.github.io/") } - val authors = view?.findViewById(R.id.authors) - authors?.setOnClickListener { + val authors = view.findViewById(R.id.authors) + authors.setOnClickListener { openLinkFromHref("https://github.com/libre-tube/LibreTube/graphs/contributors") } - val donate = view?.findViewById(R.id.donate) - donate?.setOnClickListener { + val donate = view.findViewById(R.id.donate) + donate.setOnClickListener { openLinkFromHref("https://libre-tube.github.io/#donate") } - val contributing = view?.findViewById(R.id.contributing) - contributing?.setOnClickListener { + val contributing = view.findViewById(R.id.contributing) + contributing.setOnClickListener { openLinkFromHref("https://github.com/libre-tube/LibreTube") } val license = view.findViewById(R.id.license) - license?.setOnClickListener { - val licenseString = view?.context?.assets!! + license.setOnClickListener { + val licenseString = view.context.assets .open("gpl3.html").bufferedReader().use { it.readText() } - val licenseHtml = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(licenseString, 1) - else Html.fromHtml(licenseString) + val licenseHtml = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(licenseString, 1) + } else { + Html.fromHtml(licenseString) + } - MaterialAlertDialogBuilder(view?.context!!) + MaterialAlertDialogBuilder(view.context!!) .setPositiveButton(getString(R.string.okay)) { _, _ -> } .setMessage(licenseHtml) .create() .show() - true } } diff --git a/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt b/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt index 6e125f17b..430f89944 100644 --- a/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt @@ -13,7 +13,6 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityCompat -import androidx.core.app.ActivityCompat.recreate import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -30,7 +29,6 @@ import java.io.IOException import java.io.InputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream -import org.chromium.base.ThreadUtils.runOnUiThread import org.json.JSONObject import org.json.JSONTokener import retrofit2.HttpException diff --git a/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt b/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt index 1579feab0..1236a8b5c 100644 --- a/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt @@ -17,6 +17,9 @@ class SponsorBlockSettings : PreferenceFragmentCompat() { var interactionEnabled: Boolean = false var introEnabled: Boolean = false var outroEnabled: Boolean = false + var fillerEnabled: Boolean = false + var musicOfftopicEnabled: Boolean = false + var previewEnabled: Boolean = false } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -65,5 +68,23 @@ class SponsorBlockSettings : PreferenceFragmentCompat() { outroEnabled = newValue as Boolean true } + + val fillerToggle = findPreference("filler_category_key") + fillerToggle?.setOnPreferenceChangeListener { _, newValue -> + fillerEnabled = newValue as Boolean + true + } + + val musicToggle = findPreference("music_offtopic_category_key") + musicToggle?.setOnPreferenceChangeListener { _, newValue -> + musicOfftopicEnabled = newValue as Boolean + true + } + + val previewToggle = findPreference("preview_category_key") + previewToggle?.setOnPreferenceChangeListener { _, newValue -> + previewEnabled = newValue as Boolean + true + } } } diff --git a/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt b/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt new file mode 100644 index 000000000..bd9de4aea --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt @@ -0,0 +1,55 @@ +package com.github.libretube.util + +import android.app.PendingIntent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.ui.PlayerNotificationManager +import java.net.URL + +// used to show title and thumbnail of the video in the notification +class DescriptionAdapter( + private val title: String, + private val channelName: String, + private val thumbnailUrl: String +) : + PlayerNotificationManager.MediaDescriptionAdapter { + override fun getCurrentContentTitle(player: Player): CharSequence { + // return controller.metadata.description.title.toString() + return title + } + + override fun createCurrentContentIntent(player: Player): PendingIntent? { + // return controller.sessionActivity + return null + } + + override fun getCurrentContentText(player: Player): CharSequence? { + // return controller.metadata.description.subtitle.toString() + return channelName + } + + override fun getCurrentLargeIcon( + player: Player, + callback: PlayerNotificationManager.BitmapCallback + ): Bitmap? { + lateinit var bitmap: Bitmap + val thread = Thread { + try { + // try to parse the thumbnailUrl to a Bitmap + val inputStream = URL(thumbnailUrl).openStream() + bitmap = BitmapFactory.decodeStream(inputStream) + } catch (ex: java.lang.Exception) { + ex.printStackTrace() + } + } + thread.start() + thread.join() + // return bitmap if initialized + return try { + bitmap + } catch (e: Exception) { + null + } + } +} diff --git a/app/src/main/res/drawable/ic_empty_playlist.xml b/app/src/main/res/drawable/ic_empty_playlist.xml index 819452cc1..3b7371ebc 100644 --- a/app/src/main/res/drawable/ic_empty_playlist.xml +++ b/app/src/main/res/drawable/ic_empty_playlist.xml @@ -3,9 +3,9 @@ android:height="144dp" android:viewportWidth="143" android:viewportHeight="144" - android:tint="?android:attr/colorControlNormal" > - + android:tint="?android:attr/colorControlNormal"> + diff --git a/app/src/main/res/layout/chapter_column.xml b/app/src/main/res/layout/chapter_column.xml new file mode 100644 index 000000000..4f5171ea4 --- /dev/null +++ b/app/src/main/res/layout/chapter_column.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/comments_row.xml b/app/src/main/res/layout/comments_row.xml index 295108ce1..d764f3b71 100644 --- a/app/src/main/res/layout/comments_row.xml +++ b/app/src/main/res/layout/comments_row.xml @@ -9,105 +9,106 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> - - - + android:paddingBottom="16dp"> + + - - - - - - - - - + android:orientation="vertical"> - + - + + + + + + + android:layout_marginBottom="4dp" + android:autoLink="web" + android:text="Comment Text" /> - + + + + + + + + - - + diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 7d9f03a4d..63936c4c3 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -39,7 +39,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:paddingHorizontal="20dp" android:paddingVertical="10dp"> + android:paddingHorizontal="20dp" + android:paddingVertical="10dp"> + android:paddingHorizontal="20dp" + android:paddingVertical="10dp"> + android:paddingHorizontal="20dp" + android:paddingVertical="10dp"> + android:paddingHorizontal="20dp" + android:paddingVertical="10dp"> + android:paddingHorizontal="20dp" + android:paddingVertical="10dp"> - + + + + + + + + + + + + + + + android:nestedScrollingEnabled="false" + android:visibility="gone" /> + android:text="@string/download" /> diff --git a/app/src/main/res/layout/searchsuggestion_row.xml b/app/src/main/res/layout/searchsuggestion_row.xml index 09200c426..f8cd7454d 100644 --- a/app/src/main/res/layout/searchsuggestion_row.xml +++ b/app/src/main/res/layout/searchsuggestion_row.xml @@ -1,10 +1,9 @@ An interval without actual content. Could be a pause, static frame, repeating animation. Should not be used for transitions containing info. End cards and credits Info following the ending. Not for conclusions with info. + Filler Tangent/Jokes + This is for tangential scenes added only for filler or humor that are not required to understand the main content of the video. + Music: Non-Music Section + This is only for use in music videos. It should cover parts of the video not part of official mixes. In the end, the video should resemble the Spotify or any other mix version as closely as possible, or should reduce talking or other distractions. + Preview/Recap + For segments that show what is coming up in this or future videos of the same series, but do not provide additional information. If it includes clips that only appear here, this is very likely not the right category. License Accents Resting red @@ -173,4 +179,6 @@ Get to know team LibreTube and how it all happens. Related streams Show related streams to videos. + Show chapters + Hide chapters diff --git a/app/src/main/res/xml/advanced_settings.xml b/app/src/main/res/xml/advanced_settings.xml index 957631450..87546c389 100644 --- a/app/src/main/res/xml/advanced_settings.xml +++ b/app/src/main/res/xml/advanced_settings.xml @@ -5,21 +5,21 @@ @@ -27,55 +27,44 @@ + app:title="@string/video_format" /> + app:icon="@drawable/ic_download" + app:key="download_location" + app:summary="@string/download_directory_summary" + app:title="@string/download_directory" /> + app:key="download_folder" + app:summary="@string/download_folder_summary" + app:title="@string/download_folder" /> + android:icon="@drawable/ic_history" + app:key="search_history_toggle" + app:title="@string/search_history" /> - - - - - - + app:title="@string/clear_history" /> diff --git a/app/src/main/res/xml/appearance_settings.xml b/app/src/main/res/xml/appearance_settings.xml index 592084d19..5b52bb864 100644 --- a/app/src/main/res/xml/appearance_settings.xml +++ b/app/src/main/res/xml/appearance_settings.xml @@ -5,30 +5,30 @@ @@ -36,23 +36,34 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sponsorblock_settings.xml b/app/src/main/res/xml/sponsorblock_settings.xml index 395229c7c..8b9e138c8 100644 --- a/app/src/main/res/xml/sponsorblock_settings.xml +++ b/app/src/main/res/xml/sponsorblock_settings.xml @@ -41,6 +41,21 @@ app:title="@string/category_outro" app:summary="@string/category_outro_description" /> + + + + + + diff --git a/build.gradle b/build.gradle index 738ae3345..0a229bb5e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:7.2.1' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files