Merge branch 'master' into patch-5

This commit is contained in:
Allan Nordhøy 2022-08-03 15:29:36 +02:00 committed by GitHub
commit 9be8f5549b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 314 additions and 433 deletions

View File

@ -97,8 +97,6 @@ dependencies {
// Do not update jackson annotations! It does not supports < API 26.
implementation libs.jacksonAnnotations
implementation libs.mobileffmpeg
coreLibraryDesugaring libs.desugaring
implementation libs.cronet.embedded
implementation libs.cronet.okhttp

View File

@ -193,8 +193,10 @@ class MainActivity : AppCompatActivity() {
})
searchView.setOnCloseListener {
onBackPressed()
true
if (navController.currentDestination?.id == R.id.searchFragment) {
onBackPressed()
}
false
}
return super.onCreateOptionsMenu(menu)
}

View File

@ -13,18 +13,15 @@ import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.dialogs.PlaylistOptionsDialog
import com.github.libretube.dialogs.VideoOptionsDialog
import com.github.libretube.obj.SearchItem
import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort
import com.github.libretube.util.setWatchProgressLength
import com.github.libretube.util.toID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
class SearchAdapter(
private val searchItems: MutableList<SearchItem>,
@ -128,80 +125,41 @@ class SearchAdapter(
NavigationHelper.navigateChannel(root.context, item.url)
}
val channelId = item.url.toID()
val token = PreferenceHelper.getToken()
// only show subscribe button if logged in
if (token != "") isSubscribed(channelId, token, binding)
isSubscribed(channelId, binding)
}
}
private fun isSubscribed(channelId: String, token: String, binding: ChannelRowBinding) {
var isSubscribed = false
private fun isSubscribed(channelId: String, binding: ChannelRowBinding) {
// check whether the user subscribed to the channel
CoroutineScope(Dispatchers.Main).launch {
val response = try {
RetrofitInstance.authApi.isSubscribed(
channelId,
token
)
} catch (e: Exception) {
return@launch
}
var isSubscribed = SubscriptionHelper.isSubscribed(channelId)
// if subscribed change text to unsubscribe
if (response.subscribed == true) {
isSubscribed = true
if (isSubscribed == true) {
binding.searchSubButton.text = binding.root.context.getString(R.string.unsubscribe)
}
// make sub button visible and set the on click listeners to (un)subscribe
if (response.subscribed != null) {
binding.searchSubButton.visibility = View.VISIBLE
if (isSubscribed == null) return@launch
binding.searchSubButton.visibility = View.VISIBLE
binding.searchSubButton.setOnClickListener {
if (!isSubscribed) {
subscribe(token, channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.unsubscribe)
isSubscribed = true
} else {
unsubscribe(token, channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.subscribe)
isSubscribed = false
}
binding.searchSubButton.setOnClickListener {
if (isSubscribed == false) {
SubscriptionHelper.subscribe(channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.unsubscribe)
isSubscribed = true
} else {
SubscriptionHelper.unsubscribe(channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.subscribe)
isSubscribed = false
}
}
}
}
private fun subscribe(token: String, channelId: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
return@launch
}
}
}
private fun unsubscribe(token: String, channelId: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channelId)
)
} catch (e: IOException) {
return@launch
}
}
}
private fun bindPlaylist(item: SearchItem, binding: PlaylistSearchRowBinding) {
binding.apply {
ConnectionHelper.loadImage(item.thumbnail, searchThumbnail)

View File

@ -1,21 +1,15 @@
package com.github.libretube.adapters
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
import com.github.libretube.obj.Subscribe
import com.github.libretube.obj.Subscription
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.toID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscription>) :
RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
@ -46,51 +40,17 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscrip
val channelId = subscription.url.toID()
if (subscribed) {
subscriptionSubscribe.text = root.context.getString(R.string.subscribe)
unsubscribe(channelId)
SubscriptionHelper.unsubscribe(channelId)
subscribed = false
} else {
subscriptionSubscribe.text =
root.context.getString(R.string.unsubscribe)
subscribe(channelId)
SubscriptionHelper.subscribe(channelId)
subscribed = true
}
}
}
}
private fun subscribe(channelId: String) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
try {
val token = PreferenceHelper.getToken()
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
}
}
run()
}
private fun unsubscribe(channelId: String) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
try {
val token = PreferenceHelper.getToken()
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
}
}
run()
}
}
class SubscriptionChannelViewHolder(val binding: ChannelSubscriptionRowBinding) :

View File

@ -4,6 +4,7 @@ import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
@ -41,6 +42,16 @@ class DownloadDialog : DialogFragment() {
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
binding.audioRadio.setOnClickListener {
binding.videoSpinner.visibility = View.GONE
binding.audioSpinner.visibility = View.VISIBLE
}
binding.videoRadio.setOnClickListener {
binding.audioSpinner.visibility = View.GONE
binding.videoSpinner.visibility = View.VISIBLE
}
builder.setView(binding.root)
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
@ -118,14 +129,15 @@ class DownloadDialog : DialogFragment() {
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
binding.download.setOnClickListener {
val selectedAudioUrl = audioUrl[binding.audioSpinner.selectedItemPosition]
val selectedVideoUrl = vidUrl[binding.videoSpinner.selectedItemPosition]
val selectedAudioUrl =
if (binding.audioRadio.isChecked) audioUrl[binding.audioSpinner.selectedItemPosition] else ""
val selectedVideoUrl =
if (binding.videoRadio.isChecked) vidUrl[binding.videoSpinner.selectedItemPosition] else ""
val intent = Intent(context, DownloadService::class.java)
intent.putExtra("videoId", videoId)
intent.putExtra("videoName", streams.title)
intent.putExtra("videoUrl", selectedVideoUrl)
intent.putExtra("audioUrl", selectedAudioUrl)
intent.putExtra("duration", duration)
context?.startService(intent)
dismiss()
}

View File

@ -13,7 +13,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ShareDialog(
private val id: String,
private val isPlaylist: Boolean
private val isPlaylist: Boolean,
private val position: Long = 0L
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -39,7 +40,14 @@ class ShareDialog(
else -> instanceUrl
}
val path = if (!isPlaylist) "/watch?v=$id" else "/playlist?list=$id"
val url = "$host$path"
var url = "$host$path"
if (PreferenceHelper.getBoolean(
PreferenceKeys.SHARE_WITH_TIME_CODE,
true
)
) {
url += "?t=$position"
}
val intent = Intent()
intent.apply {

View File

@ -5,17 +5,15 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.adapters.ChannelAdapter
import com.github.libretube.databinding.FragmentChannelBinding
import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort
import com.github.libretube.util.toID
import retrofit2.HttpException
@ -31,7 +29,7 @@ class ChannelFragment : Fragment() {
var nextPage: String? = null
private var channelAdapter: ChannelAdapter? = null
private var isLoading = true
private var isSubscribed: Boolean = false
private var isSubscribed: Boolean? = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -61,13 +59,7 @@ class ChannelFragment : Fragment() {
val refreshChannel = {
binding.channelRefresh.isRefreshing = true
fetchChannel()
if (PreferenceHelper.getToken() != "") {
isSubscribed()
} else {
binding.channelSubscribe.setOnClickListener {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
}
}
isSubscribed()
}
refreshChannel()
binding.channelRefresh.setOnRefreshListener {
@ -90,76 +82,28 @@ class ChannelFragment : Fragment() {
}
private fun isSubscribed() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
val token = PreferenceHelper.getToken()
RetrofitInstance.authApi.isSubscribed(
channelId!!,
token
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
return@launchWhenCreated
lifecycleScope.launchWhenCreated {
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated
runOnUiThread {
if (isSubscribed == true) {
binding.channelSubscribe.text = getString(R.string.unsubscribe)
}
runOnUiThread {
if (response.subscribed == true) {
binding.channelSubscribe.setOnClickListener {
binding.channelSubscribe.text = if (isSubscribed == true) {
SubscriptionHelper.unsubscribe(channelId!!)
isSubscribed = false
getString(R.string.subscribe)
} else {
SubscriptionHelper.subscribe(channelId!!)
isSubscribed = true
binding.channelSubscribe.text = getString(R.string.unsubscribe)
}
binding.channelSubscribe.setOnClickListener {
if (response.subscribed != null) {
binding.channelSubscribe.text = if (isSubscribed) {
unsubscribe()
getString(R.string.subscribe)
} else {
subscribe()
getString(R.string.unsubscribe)
}
}
getString(R.string.unsubscribe)
}
}
}
}
run()
}
private fun subscribe() {
fun run() {
lifecycleScope.launchWhenCreated {
try {
val token = PreferenceHelper.getToken()
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
isSubscribed = true
}
}
run()
}
private fun unsubscribe() {
fun run() {
lifecycleScope.launchWhenCreated {
try {
val token = PreferenceHelper.getToken()
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
isSubscribed = false
}
}
run()
}
private fun fetchChannel() {

View File

@ -53,9 +53,7 @@ import com.github.libretube.obj.ChapterSegment
import com.github.libretube.obj.Playlist
import com.github.libretube.obj.Segment
import com.github.libretube.obj.Segments
import com.github.libretube.obj.StreamItem
import com.github.libretube.obj.Streams
import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.services.BackgroundMode
@ -66,6 +64,7 @@ import com.github.libretube.util.DescriptionAdapter
import com.github.libretube.util.OnDoubleTapEventListener
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort
import com.github.libretube.util.hideKeyboard
import com.github.libretube.util.toID
@ -117,8 +116,9 @@ class PlayerFragment : Fragment() {
private var videoId: String? = null
private var playlistId: String? = null
private var channelId: String? = null
private var isSubscribed: Boolean = false
private var isSubscribed: Boolean? = false
private var isLive = false
private lateinit var streams: Streams
/**
* for the transition
@ -175,7 +175,6 @@ class PlayerFragment : Fragment() {
/**
* for autoplay
*/
private var relatedStreams: List<StreamItem>? = arrayListOf()
private var nextStreamId: String? = null
private var playlistStreamIds: MutableList<String> = arrayListOf()
private var playlistNextPage: String? = null
@ -187,13 +186,6 @@ class PlayerFragment : Fragment() {
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotification: PlayerNotificationManager
/**
* for the media description of the notification
*/
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 {
@ -540,7 +532,7 @@ class PlayerFragment : Fragment() {
// share button
binding.relPlayerShare.setOnClickListener {
val shareDialog = ShareDialog(videoId!!, false)
val shareDialog = ShareDialog(videoId!!, false, exoPlayer.currentPosition)
shareDialog.show(childFragmentManager, "ShareDialog")
}
@ -739,7 +731,7 @@ class PlayerFragment : Fragment() {
private fun playVideo() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
streams = try {
RetrofitInstance.api.getStreams(videoId!!)
} catch (e: IOException) {
println(e)
@ -751,21 +743,13 @@ 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!!
channelId = response.uploaderUrl.toID()
// save related streams for autoplay
relatedStreams = response.relatedStreams
runOnUiThread {
// set media sources for the player
setResolutionAndSubtitles(response)
setResolutionAndSubtitles(streams)
prepareExoPlayerView()
initializePlayerView(response)
seekToWatchPosition()
initializePlayerView(streams)
if (!isLive) seekToWatchPosition()
exoPlayer.prepare()
exoPlayer.play()
exoPlayerView.useController = true
@ -776,7 +760,7 @@ class PlayerFragment : Fragment() {
// prepare for autoplay
initAutoPlay()
if (watchHistoryEnabled) {
PreferenceHelper.addToWatchHistory(videoId!!, response)
PreferenceHelper.addToWatchHistory(videoId!!, streams)
}
}
}
@ -828,7 +812,10 @@ class PlayerFragment : Fragment() {
val watchPositions = PreferenceHelper.getWatchPositions()
var position: Long? = null
watchPositions.forEach {
if (it.videoId == videoId) position = it.position
if (it.videoId == videoId &&
// don't seek to the position if it's the end, autoplay would skip it immediately
streams.duration!! - it.position / 1000 > 2
) position = it.position
}
// support for time stamped links
val timeStamp: Long? = arguments?.getLong("timeStamp")
@ -884,9 +871,9 @@ class PlayerFragment : Fragment() {
// else: the video must be the last video of the playlist so nothing happens
// if it's not a playlist then use the next related video
} else if (relatedStreams != null && relatedStreams!!.isNotEmpty()) {
} else if (streams.relatedStreams != null && streams.relatedStreams!!.isNotEmpty()) {
// save next video from related streams for autoplay
nextStreamId = relatedStreams!![0].url.toID()
nextStreamId = streams.relatedStreams!![0].url.toID()
}
}
}
@ -896,6 +883,9 @@ class PlayerFragment : Fragment() {
// check whether there is a new video in the queue
// by making sure that the next and the current video aren't the same
saveWatchPosition()
// forces the comments to reload for the new video
commentsLoaded = false
binding.commentsRecView.adapter = null
if (videoId != nextStreamId) {
// save the id of the next stream as videoId and load the next video
videoId = nextStreamId
@ -1064,9 +1054,9 @@ class PlayerFragment : Fragment() {
intent.action = Intent.ACTION_VIEW
intent.setDataAndType(uri, "video/*")
intent.putExtra(Intent.EXTRA_TITLE, title)
intent.putExtra("title", title)
intent.putExtra("artist", uploader)
intent.putExtra(Intent.EXTRA_TITLE, streams.title)
intent.putExtra("title", streams.title)
intent.putExtra("artist", streams.uploader)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
@ -1278,7 +1268,7 @@ class PlayerFragment : Fragment() {
// get the name of the currently played chapter
private fun getCurrentChapterIndex(): Int {
val currentPosition = exoPlayer.currentPosition
var chapterIndex: Int? = null
var chapterIndex = 0
chapters.forEachIndexed { index, chapter ->
// check whether the chapter start is greater than the current player position
@ -1287,7 +1277,7 @@ class PlayerFragment : Fragment() {
chapterIndex = index
}
}
return chapterIndex!!
return chapterIndex
}
private fun setMediaSource(
@ -1525,7 +1515,12 @@ class PlayerFragment : Fragment() {
playerNotification = PlayerNotificationManager
.Builder(c, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
.setMediaDescriptionAdapter(
DescriptionAdapter(title, uploader, thumbnailUrl, requireContext())
DescriptionAdapter(
streams.title!!,
streams.uploader!!,
streams.thumbnailUrl!!,
requireContext()
)
)
.build()
@ -1564,38 +1559,22 @@ class PlayerFragment : Fragment() {
private fun isSubscribed() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.isSubscribed(
channelId!!,
token
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated
runOnUiThread {
if (response.subscribed == true) {
isSubscribed = true
if (isSubscribed == true) {
binding.playerSubscribe.text = getString(R.string.unsubscribe)
}
if (response.subscribed != null) {
binding.playerSubscribe.setOnClickListener {
if (isSubscribed) {
unsubscribe(channelId!!)
binding.playerSubscribe.text = getString(R.string.subscribe)
} else {
subscribe(channelId!!)
binding.playerSubscribe.text = getString(R.string.unsubscribe)
}
binding.playerSubscribe.setOnClickListener {
if (isSubscribed == true) {
SubscriptionHelper.unsubscribe(channelId!!)
binding.playerSubscribe.text = getString(R.string.subscribe)
} else {
SubscriptionHelper.subscribe(channelId!!)
binding.playerSubscribe.text = getString(R.string.unsubscribe)
}
} else {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT)
.show()
}
}
}
@ -1603,50 +1582,6 @@ class PlayerFragment : Fragment() {
run()
}
private fun subscribe(channelId: String) {
fun run() {
lifecycleScope.launchWhenCreated {
try {
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response$e")
return@launchWhenCreated
}
isSubscribed = true
}
}
run()
}
private fun unsubscribe(channel_id: String) {
fun run() {
lifecycleScope.launchWhenCreated {
try {
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channel_id)
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
isSubscribed = false
}
}
run()
}
private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return
if (!isAdded) return // Fragment not attached to an Activity

View File

@ -19,6 +19,7 @@ import com.github.libretube.obj.StreamItem
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.toID
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
@ -53,63 +54,57 @@ class SubscriptionsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
token = PreferenceHelper.getToken()
if (token != "") {
binding.loginOrRegister.visibility = View.GONE
binding.subRefresh.isEnabled = true
binding.subRefresh.isEnabled = true
binding.subProgress.visibility = View.VISIBLE
binding.subProgress.visibility = View.VISIBLE
val grid = PreferenceHelper.getString(
PreferenceKeys.GRID_COLUMNS,
resources.getInteger(R.integer.grid_items).toString()
)
binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
val grid = PreferenceHelper.getString(
PreferenceKeys.GRID_COLUMNS,
resources.getInteger(R.integer.grid_items).toString()
)
binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
fetchFeed()
binding.subRefresh.setOnRefreshListener {
fetchChannels()
fetchFeed()
binding.subRefresh.setOnRefreshListener {
fetchChannels()
fetchFeed()
}
binding.sortTV.setOnClickListener {
showSortDialog()
}
binding.toggleSubs.visibility = View.VISIBLE
var loadedSubbedChannels = false
binding.toggleSubs.setOnClickListener {
if (!binding.subChannelsContainer.isVisible) {
if (!loadedSubbedChannels) {
binding.subChannels.layoutManager = LinearLayoutManager(context)
fetchChannels()
loadedSubbedChannels = true
}
binding.subChannelsContainer.visibility = View.VISIBLE
binding.subFeedContainer.visibility = View.GONE
} else {
binding.subChannelsContainer.visibility = View.GONE
binding.subFeedContainer.visibility = View.VISIBLE
}
}
binding.scrollviewSub.viewTreeObserver
.addOnScrollChangedListener {
if (binding.scrollviewSub.getChildAt(0).bottom
== (binding.scrollviewSub.height + binding.scrollviewSub.scrollY)
) {
// scroll view is at bottom
if (isLoaded) {
binding.subRefresh.isRefreshing = true
subscriptionAdapter?.updateItems()
binding.subRefresh.isRefreshing = false
}
}
}
} else {
binding.subRefresh.isEnabled = false
binding.subFeedContainer.visibility = View.GONE
}
binding.sortTV.setOnClickListener {
showSortDialog()
}
binding.toggleSubs.visibility = View.VISIBLE
var loadedSubbedChannels = false
binding.toggleSubs.setOnClickListener {
if (!binding.subChannelsContainer.isVisible) {
if (!loadedSubbedChannels) {
binding.subChannels.layoutManager = LinearLayoutManager(context)
fetchChannels()
loadedSubbedChannels = true
}
binding.subChannelsContainer.visibility = View.VISIBLE
binding.subFeedContainer.visibility = View.GONE
} else {
binding.subChannelsContainer.visibility = View.GONE
binding.subFeedContainer.visibility = View.VISIBLE
}
}
binding.scrollviewSub.viewTreeObserver
.addOnScrollChangedListener {
if (binding.scrollviewSub.getChildAt(0).bottom
== (binding.scrollviewSub.height + binding.scrollviewSub.scrollY)
) {
// scroll view is at bottom
if (isLoaded) {
binding.subRefresh.isRefreshing = true
subscriptionAdapter?.updateItems()
binding.subRefresh.isRefreshing = false
}
}
}
}
private fun showSortDialog() {
@ -130,7 +125,10 @@ class SubscriptionsFragment : Fragment() {
fun run() {
lifecycleScope.launchWhenCreated {
feed = try {
RetrofitInstance.authApi.getFeed(token)
if (token != "") RetrofitInstance.authApi.getFeed(token)
else RetrofitInstance.authApi.getUnauthenticatedFeed(
SubscriptionHelper.getFormattedLocalSubscriptions()
)
} catch (e: IOException) {
Log.e(TAG, e.toString())
Log.e(TAG, "IOException, you might not have internet connection")
@ -148,15 +146,7 @@ class SubscriptionsFragment : Fragment() {
showFeed()
} else {
runOnUiThread {
with(binding.boogh) {
visibility = View.VISIBLE
setImageResource(R.drawable.ic_list)
}
with(binding.textLike) {
visibility = View.VISIBLE
text = getString(R.string.emptyList)
}
binding.loginOrRegister.visibility = View.VISIBLE
binding.emptyFeed.visibility = View.VISIBLE
}
}
binding.subProgress.visibility = View.GONE
@ -185,7 +175,10 @@ class SubscriptionsFragment : Fragment() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.subscriptions(token)
if (token != "") RetrofitInstance.authApi.subscriptions(token)
else RetrofitInstance.authApi.unauthenticatedSubscriptions(
SubscriptionHelper.getFormattedLocalSubscriptions()
)
} catch (e: IOException) {
Log.e(TAG, e.toString())
Log.e(TAG, "IOException, you might not have internet connection")

View File

@ -235,6 +235,21 @@ object PreferenceHelper {
return getString(PreferenceKeys.ERROR_LOG, "")
}
fun getLocalSubscriptions(): List<String> {
val json = settings.getString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, "")
return try {
val type = object : TypeReference<List<String>>() {}
mapper.readValue(json, type)
} catch (e: Exception) {
listOf()
}
}
fun setLocalSubscriptions(channels: List<String>) {
val json = mapper.writeValueAsString(channels)
editor.putString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, json).commit()
}
private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}

View File

@ -61,7 +61,6 @@ object PreferenceKeys {
/**
* Download
*/
const val DOWNLOAD_VIDEO_FORMAT = "video_format"
const val DOWNLOAD_LOCATION = "download_location"
const val DOWNLOAD_FOLDER = "download_folder"
@ -81,9 +80,15 @@ object PreferenceKeys {
const val CLEAR_SEARCH_HISTORY = "clear_search_history"
const val CLEAR_WATCH_HISTORY = "clear_watch_history"
const val CLEAR_WATCH_POSITIONS = "clear_watch_positions"
const val SHARE_WITH_TIME_CODE = "share_with_time_code"
/**
* Error logs
*/
const val ERROR_LOG = "error_log"
/**
* Data
*/
const val LOCAL_SUBSCRIPTIONS = "local_subscriptions"
}

View File

@ -17,7 +17,6 @@ import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.arthenica.ffmpegkit.FFmpegKit
import com.github.libretube.DOWNLOAD_CHANNEL_ID
import com.github.libretube.DOWNLOAD_FAILURE_NOTIFICATION_ID
import com.github.libretube.DOWNLOAD_PENDING_NOTIFICATION_ID
@ -35,11 +34,9 @@ class DownloadService : Service() {
private lateinit var notification: NotificationCompat.Builder
private var downloadId: Long = -1
private lateinit var videoId: String
private lateinit var videoName: String
private lateinit var videoUrl: String
private lateinit var audioUrl: String
private lateinit var extension: String
private var duration: Int = 0
private var downloadType: Int = 3
private lateinit var audioDir: File
@ -52,13 +49,11 @@ class DownloadService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
videoId = intent?.getStringExtra("videoId")!!
videoName = intent?.getStringExtra("videoName")!!
videoUrl = intent.getStringExtra("videoUrl")!!
audioUrl = intent.getStringExtra("audioUrl")!!
duration = intent.getIntExtra("duration", 1)
extension = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_VIDEO_FORMAT, ".mp4")!!
downloadType = if (audioUrl != "" && videoUrl != "") DownloadType.MUX
else if (audioUrl != "") DownloadType.AUDIO
downloadType = if (audioUrl != "") DownloadType.AUDIO
else if (videoUrl != "") DownloadType.VIDEO
else DownloadType.NONE
if (downloadType != DownloadType.NONE) {
@ -115,18 +110,8 @@ class DownloadService : Service() {
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
when (downloadType) {
DownloadType.MUX -> {
audioDir = File(tempDir, "$videoId-audio")
videoDir = File(tempDir, "$videoId-video")
downloadId = downloadManagerRequest(
getString(R.string.video),
getString(R.string.downloading),
videoUrl,
videoDir
)
}
DownloadType.VIDEO -> {
videoDir = File(libretubeDir, "$videoId-video")
videoDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest(
getString(R.string.video),
getString(R.string.downloading),
@ -135,7 +120,7 @@ class DownloadService : Service() {
)
}
DownloadType.AUDIO -> {
audioDir = File(libretubeDir, "$videoId-audio")
audioDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest(
getString(R.string.audio),
getString(R.string.downloading),
@ -146,6 +131,7 @@ class DownloadService : Service() {
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "download error $e")
downloadFailedNotification()
}
}
@ -166,11 +152,6 @@ class DownloadService : Service() {
downloadSucceededNotification()
onDestroy()
}
} else {
try {
muxDownloadedMedia()
} catch (e: Exception) {
}
}
}
}
@ -233,7 +214,7 @@ class DownloadService : Service() {
val builder = NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle(resources.getString(R.string.success))
.setContentText(getString(R.string.fail))
.setContentText(getString(R.string.downloadsucceeded))
.setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define
@ -241,39 +222,6 @@ class DownloadService : Service() {
}
}
private fun muxDownloadedMedia() {
val command = "-y -i $videoDir -i $audioDir -c copy $libretubeDir/${videoId}$extension"
notification.setContentTitle("Muxing")
FFmpegKit.executeAsync(
command,
{ session ->
val state = session.state
val returnCode = session.returnCode
// CALLED WHEN SESSION IS EXECUTED
Log.d(
TAG,
String.format(
"FFmpeg process exited with state %s and rc %s.%s",
state,
returnCode,
session.failStackTrace
)
)
tempDir.deleteRecursively()
if (returnCode.toString() != "0") downloadFailedNotification()
else downloadSucceededNotification()
onDestroy()
},
{
// CALLED WHEN SESSION PRINTS LOGS
Log.e(TAG, it.message.toString())
}
) {
// CALLED WHEN SESSION GENERATES STATISTICS
Log.e(TAG + "stat", it.time.toString())
}
}
override fun onDestroy() {
try {
unregisterReceiver(onDownloadComplete)

View File

@ -99,6 +99,9 @@ interface PipedApi {
@GET("feed")
suspend fun getFeed(@Query("authToken") token: String?): List<StreamItem>
@GET("feed/unauthenticated")
suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List<StreamItem>
@GET("subscribed")
suspend fun isSubscribed(
@Query("channelId") channelId: String,
@ -108,6 +111,9 @@ interface PipedApi {
@GET("subscriptions")
suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription>
@GET("subscriptions/unauthenticated")
suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List<Subscription>
@POST("subscribe")
suspend fun subscribe(
@Header("Authorization") token: String,

View File

@ -0,0 +1,71 @@
package com.github.libretube.util
import android.util.Log
import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object SubscriptionHelper {
val TAG = "SubscriptionHelper"
fun subscribe(channelId: String) {
if (PreferenceHelper.getToken() != "") {
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.subscribe(
PreferenceHelper.getToken(),
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
}
} else {
val channels = PreferenceHelper.getLocalSubscriptions().toMutableList()
channels.add(channelId)
PreferenceHelper.setLocalSubscriptions(channels)
}
}
fun unsubscribe(channelId: String) {
if (PreferenceHelper.getToken() != "") {
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.unsubscribe(
PreferenceHelper.getToken(),
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
}
} else {
val channels = PreferenceHelper.getLocalSubscriptions().toMutableList()
channels.remove(channelId)
PreferenceHelper.setLocalSubscriptions(channels)
}
}
suspend fun isSubscribed(channelId: String): Boolean? {
if (PreferenceHelper.getToken() != "") {
val isSubscribed = try {
RetrofitInstance.authApi.isSubscribed(
channelId,
PreferenceHelper.getToken()
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
return null
}
return isSubscribed.subscribed
} else {
return PreferenceHelper.getLocalSubscriptions().contains(channelId)
}
}
fun getFormattedLocalSubscriptions(): String {
return PreferenceHelper.getLocalSubscriptions().joinToString(",")
}
}

View File

@ -4,7 +4,6 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
@ -14,6 +13,28 @@
android:text="@string/app_name"
android:textSize="20sp" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:checkedButton="@id/video_radio"
android:orientation="horizontal">
<RadioButton
android:id="@+id/video_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/video" />
<RadioButton
android:id="@+id/audio_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@string/audio" />
</RadioGroup>
<Spinner
android:id="@+id/video_spinner"
android:layout_width="match_parent"
@ -24,12 +45,13 @@
android:id="@+id/audio_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp" />
android:layout_margin="8dp"
android:visibility="gone" />
<Button
android:id="@+id/download"
style="@style/CustomDialogButton"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:text="@string/download" />
</LinearLayout>

View File

@ -15,10 +15,11 @@
android:visibility="gone" />
<RelativeLayout
android:id="@+id/loginOrRegister"
android:id="@+id/emptyFeed"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
android:layout_centerInParent="true"
android:visibility="gone">
<ImageView
android:id="@+id/boogh"
@ -26,7 +27,7 @@
android:layout_height="100dp"
android:layout_centerInParent="true"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_login" />
android:src="@drawable/ic_list" />
<TextView
android:id="@+id/textLike"
@ -36,7 +37,7 @@
android:layout_centerHorizontal="true"
android:layout_marginHorizontal="10dp"
android:gravity="center"
android:text="@string/please_login"
android:text="@string/emptyList"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>

View File

@ -289,4 +289,6 @@
<string name="no_search_result">No results.</string>
<string name="error_occurred">Error</string>
<string name="copied">Copied</string>
<string name="downloadsucceeded">Downloaded</string>
<string name="share_with_time">Share with start time</string>
</resources>

View File

@ -4,15 +4,6 @@
<PreferenceCategory app:title="@string/downloads">
<ListPreference
app:defaultValue=".mp4"
app:entries="@array/videoFormats"
app:entryValues="@array/videoFormatsValues"
app:icon="@drawable/ic_videocam"
app:key="video_format"
app:summary="@string/video_format_summary"
app:title="@string/video_format" />
<ListPreference
android:defaultValue="downloads"
android:entries="@array/downloadLocation"
@ -31,6 +22,16 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/share">
<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_time"
app:key="share_with_time_code"
app:title="@string/share_with_time" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/advanced">
<SwitchPreferenceCompat