Merge branch 'libre-tube:master' into master

This commit is contained in:
XelXen 2022-07-14 22:48:25 +05:30 committed by GitHub
commit d1547fd9b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1044 additions and 519 deletions

View File

@ -3,13 +3,13 @@
This represents the larger, bigger impact features and enhancements we have planned. Features are planned, but do not represent a commitment to develop and can change at any time.
## Contribute
Feel free to help us if you have any knowledge concerning the following planned features.
Feel free to help us if you have any knowledge concerning the following planned features or anything else you imagine.
## Planned
- Landscape mode support
- Rewrite of the Player UI
- Notifications for new streams
## Not planned
- Google/MicroG Login
- Support for anything else than Android (like iOS, Linux)
- Support for Android TV

View File

@ -37,7 +37,7 @@
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:screenOrientation="userPortrait"
android:screenOrientation="user"
android:supportsPictureInPicture="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -21,3 +21,14 @@ const val MATRIX_URL = "https://matrix.to/#/#LibreTube:matrix.org"
const val DISCORD_URL = "https://discord.com/invite/Qc34xCj2GV"
const val REDDIT_URL = "https://www.reddit.com/r/Libretube/"
const val TWITTER_URL = "https://twitter.com/libretube"
/**
* Share Dialog
*/
const val PIPED_FRONTEND_URL = "https://piped.kavin.rocks"
const val YOUTUBE_FRONTEND_URL = "https://www.youtube.com"
/**
* Retrofit Instance
*/
const val PIPED_API_URL = "https://pipedapi.kavin.rocks/"

View File

@ -0,0 +1,7 @@
package com.github.libretube
object Globals {
var isFullScreen = false
var isMiniPlayerVisible = false
var isCurrentViewMainSettings = true
}

View File

@ -3,7 +3,6 @@ package com.github.libretube.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
@ -25,10 +24,11 @@ import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import com.github.libretube.Globals
import com.github.libretube.PIPED_API_URL
import com.github.libretube.R
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.fragments.PlayerFragment
import com.github.libretube.fragments.isFullScreen
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.services.ClosingService
import com.github.libretube.util.ConnectionHelper
@ -36,8 +36,8 @@ import com.github.libretube.util.CronetHelper
import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.ThemeHelper
import com.google.android.material.color.DynamicColors
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.navigation.NavigationBarView
class MainActivity : AppCompatActivity() {
val TAG = "MainActivity"
@ -48,20 +48,9 @@ class MainActivity : AppCompatActivity() {
private var startFragmentId = R.id.homeFragment
override fun onCreate(savedInstanceState: Bundle?) {
/**
* apply dynamic colors if enabled
*/
val materialColorsEnabled = PreferenceHelper
.getString(this, "accent_color", "purple") == "my"
if (materialColorsEnabled) {
// apply dynamic colors to the current activity
DynamicColors.applyToActivityIfAvailable(this)
// apply dynamic colors to the all other activities
DynamicColors.applyToActivitiesIfAvailable(application)
}
// set the theme
// set the app theme (e.g. Material You)
ThemeHelper.updateTheme(this)
// set the language
LocaleHelper.updateLanguage(this)
@ -73,14 +62,14 @@ class MainActivity : AppCompatActivity() {
CronetHelper.initCronet(this.applicationContext)
RetrofitInstance.url =
PreferenceHelper.getString(this, "selectInstance", "https://pipedapi.kavin.rocks/")!!
PreferenceHelper.getString(this, "selectInstance", PIPED_API_URL)!!
// set auth instance
RetrofitInstance.authUrl =
if (PreferenceHelper.getBoolean(this, "auth_instance_toggle", false)) {
PreferenceHelper.getString(
this,
"selectAuthInstance",
"https://pipedapi.kavin.rocks/"
PIPED_API_URL
)!!
} else {
RetrofitInstance.url
@ -94,8 +83,6 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
navController = findNavController(R.id.fragment)
binding.bottomNav.setupWithNavController(navController)
@ -124,6 +111,16 @@ class MainActivity : AppCompatActivity() {
// navigate to the default fragment
navController.navigate(startFragmentId)
val labelVisibilityMode = when (
PreferenceHelper.getString(this, "label_visibility", "always")
) {
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
"never" -> NavigationBarView.LABEL_VISIBILITY_UNLABELED
else -> NavigationBarView.LABEL_VISIBILITY_AUTO
}
binding.bottomNav.labelVisibilityMode = labelVisibilityMode
binding.bottomNav.setOnItemSelectedListener {
// clear backstack if it's the start fragment
if (startFragmentId == it.itemId) navController.backQueue.clear()
@ -142,12 +139,6 @@ class MainActivity : AppCompatActivity() {
false
}
/**
* don't remove this line
* this prevents reselected items at the bottomNav to be duplicated in the backstack
*/
binding.bottomNav.setOnItemReselectedListener {}
binding.toolbar.title = ThemeHelper.getStyledAppName(this)
binding.toolbar.setNavigationOnClickListener {
@ -293,14 +284,15 @@ class MainActivity : AppCompatActivity() {
binding.mainMotionLayout.transitionToEnd()
findViewById<ConstraintLayout>(R.id.main_container).isClickable = false
val motionLayout = findViewById<MotionLayout>(R.id.playerMotionLayout)
// set the animation duration
motionLayout.setTransitionDuration(250)
motionLayout.transitionToEnd()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
with(motionLayout) {
getConstraintSet(R.id.start).constrainHeight(R.id.player, 0)
enableTransition(R.id.yt_transition, true)
}
findViewById<LinearLayout>(R.id.linLayout).visibility = View.VISIBLE
isFullScreen = false
Globals.isFullScreen = false
}
override fun onConfigurationChanged(newConfig: Configuration) {

View File

@ -5,25 +5,15 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.R
import com.github.libretube.databinding.ActivityNointernetBinding
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.color.DynamicColors
import com.google.android.material.snackbar.Snackbar
class NoInternetActivity : AppCompatActivity() {
private lateinit var binding: ActivityNointernetBinding
override fun onCreate(savedInstanceState: Bundle?) {
/**
* apply dynamic colors if enabled
*/
val materialColorsEnabled = PreferenceHelper
.getString(this, "accent_color", "purple") == "my"
if (materialColorsEnabled) {
DynamicColors.applyToActivityIfAvailable(this)
}
ThemeHelper.updateTheme(this)
super.onCreate(savedInstanceState)
binding = ActivityNointernetBinding.inflate(layoutInflater)

View File

@ -1,28 +1,22 @@
package com.github.libretube.activities
import android.app.NotificationManager
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.databinding.ActivitySettingsBinding
import com.github.libretube.preferences.MainSettings
import com.github.libretube.util.ThemeHelper
import com.google.android.material.color.DynamicColors
var isCurrentViewMainSettings = true
var requireMainActivityRestart = false
class SettingsActivity : AppCompatActivity() {
val TAG = "SettingsActivity"
lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
DynamicColors.applyToActivityIfAvailable(this)
ThemeHelper.updateTheme(this)
// makes the preference dialogs use material dialogs
// apply the theme for the preference dialogs
setTheme(R.style.MaterialAlertDialog)
super.onCreate(savedInstanceState)
@ -49,21 +43,11 @@ class SettingsActivity : AppCompatActivity() {
}
override fun onBackPressed() {
if (isCurrentViewMainSettings) {
if (requireMainActivityRestart) {
requireMainActivityRestart = false
// kill player notification
val nManager =
this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nManager.cancelAll()
ThemeHelper.restartMainActivity(this)
ActivityCompat.finishAffinity(this)
} else {
super.onBackPressed()
}
if (Globals.isCurrentViewMainSettings) {
super.onBackPressed()
finishAndRemoveTask()
} else {
isCurrentViewMainSettings = true
Globals.isCurrentViewMainSettings = true
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainSettings())

View File

@ -27,7 +27,7 @@ class ChaptersAdapter(
chapterTitle.text = chapter.title
root.setOnClickListener {
val chapterStart = chapter.start!!.toLong() * 1000 // s -> ms
val chapterStart = chapter.start!! * 1000 // s -> ms
exoPlayer.seekTo(chapterStart)
}
}

View File

@ -3,6 +3,7 @@ package com.github.libretube.adapters
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
@ -17,8 +18,15 @@ import com.github.libretube.dialogs.PlaylistOptionsDialog
import com.github.libretube.dialogs.VideoOptionsDialog
import com.github.libretube.fragments.PlayerFragment
import com.github.libretube.obj.SearchItem
import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.formatShort
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
class SearchAdapter(
private val searchItems: MutableList<SearchItem>,
@ -134,6 +142,78 @@ class SearchAdapter(
val bundle = bundleOf("channel_id" to item.url)
activity.navController.navigate(R.id.channelFragment, bundle)
}
val channelId = item.url?.replace("/channel/", "")!!
val token = PreferenceHelper.getToken(root.context)
// only show subscribe button if logged in
if (token != "") isSubscribed(channelId, token, binding)
}
}
private fun isSubscribed(channelId: String, token: String, binding: ChannelSearchRowBinding) {
var isSubscribed = false
// 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
}
// if subscribed change text to unsubscribe
if (response.subscribed == true) {
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
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
}
}
}
}
}
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
}
}
}

View File

@ -17,8 +17,6 @@ import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscription>) :
RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
@ -68,17 +66,14 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscrip
private fun subscribe(context: Context, channelId: String) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
val response = try {
try {
val token = PreferenceHelper.getToken(context)
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
subscribed = true
isLoading = false
@ -90,17 +85,14 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscrip
private fun unsubscribe(context: Context, channelId: String) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
val response = try {
try {
val token = PreferenceHelper.getToken(context)
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channelId)
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
subscribed = false
isLoading = false

View File

@ -7,7 +7,6 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.databinding.DialogDeleteAccountBinding
import com.github.libretube.obj.DeleteUserRequest
import com.github.libretube.preferences.PreferenceHelper
@ -55,10 +54,10 @@ class DeleteAccountDialog : DialogFragment() {
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
}
requireMainActivityRestart = true
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
logout()
dialog?.dismiss()
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
}
}
run()

View File

@ -11,6 +11,7 @@ import android.util.Log
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.view.size
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
@ -28,7 +29,6 @@ class DownloadDialog : DialogFragment() {
private val TAG = "DownloadDialog"
private lateinit var binding: DialogDownloadBinding
private lateinit var streams: Streams
private lateinit var videoId: String
private var duration = 0
@ -40,7 +40,7 @@ class DownloadDialog : DialogFragment() {
val builder = MaterialAlertDialogBuilder(it)
binding = DialogDownloadBinding.inflate(layoutInflater)
fetchStreams()
fetchAvailableSources()
// request storage permissions if not granted yet
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@ -83,10 +83,10 @@ class DownloadDialog : DialogFragment() {
} ?: throw IllegalStateException("Activity cannot be null")
}
private fun fetchStreams() {
private fun fetchAvailableSources() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getStreams(videoId!!)
RetrofitInstance.api.getStreams(videoId)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
@ -102,8 +102,8 @@ class DownloadDialog : DialogFragment() {
}
private fun initDownloadOptions(streams: Streams) {
var vidName = arrayListOf<String>()
var vidUrl = arrayListOf<String>()
val vidName = arrayListOf<String>()
val vidUrl = arrayListOf<String>()
// add empty selection
vidName.add(getString(R.string.no_video))
@ -111,13 +111,15 @@ class DownloadDialog : DialogFragment() {
// add all available video streams
for (vid in streams.videoStreams!!) {
val name = vid.quality + " " + vid.format
vidName.add(name)
vidUrl.add(vid.url!!)
if (vid.url != null) {
val name = vid.quality + " " + vid.format
vidName.add(name)
vidUrl.add(vid.url!!)
}
}
var audioName = arrayListOf<String>()
var audioUrl = arrayListOf<String>()
val audioName = arrayListOf<String>()
val audioUrl = arrayListOf<String>()
// add empty selection
audioName.add(getString(R.string.no_audio))
@ -125,11 +127,14 @@ class DownloadDialog : DialogFragment() {
// add all available audio streams
for (audio in streams.audioStreams!!) {
val name = audio.quality + " " + audio.format
audioName.add(name)
audioUrl.add(audio.url!!)
if (audio.url != null) {
val name = audio.quality + " " + audio.format
audioName.add(name)
audioUrl.add(audio.url!!)
}
}
// initialize the video sources
val videoArrayAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_item,
@ -137,8 +142,9 @@ class DownloadDialog : DialogFragment() {
)
videoArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.videoSpinner.adapter = videoArrayAdapter
binding.videoSpinner.setSelection(1)
if (binding.videoSpinner.size >= 1) binding.videoSpinner.setSelection(1)
// initialize the audio sources
val audioArrayAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_item,
@ -146,7 +152,7 @@ class DownloadDialog : DialogFragment() {
)
audioArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.audioSpinner.adapter = audioArrayAdapter
binding.audioSpinner.setSelection(1)
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
binding.download.setOnClickListener {
val selectedAudioUrl = audioUrl[binding.audioSpinner.selectedItemPosition]

View File

@ -7,7 +7,6 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.databinding.DialogLoginBinding
import com.github.libretube.obj.Login
import com.github.libretube.preferences.PreferenceHelper
@ -82,9 +81,9 @@ class LoginDialog : DialogFragment() {
Toast.makeText(context, R.string.loggedIn, Toast.LENGTH_SHORT).show()
PreferenceHelper.setToken(requireContext(), response.token!!)
PreferenceHelper.setUsername(requireContext(), login.username!!)
requireMainActivityRestart = true
val restartDialog = RequireRestartDialog()
restartDialog.show(parentFragmentManager, "RequireRestartDialog")
dialog?.dismiss()
activity?.recreate()
}
}
}

View File

@ -5,7 +5,6 @@ import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.databinding.DialogLogoutBinding
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ThemeHelper
@ -25,7 +24,6 @@ class LogoutDialog : DialogFragment() {
binding.user.text =
binding.user.text.toString() + " (" + user + ")"
binding.logout.setOnClickListener {
requireMainActivityRestart = true
Toast.makeText(context, R.string.loggedout, Toast.LENGTH_SHORT).show()
PreferenceHelper.setToken(requireContext(), "")
dialog?.dismiss()

View File

@ -0,0 +1,25 @@
package com.github.libretube.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class RequireRestartDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.require_restart)
.setMessage(R.string.require_restart_message)
.setPositiveButton(R.string.okay) { _, _ ->
activity?.recreate()
ThemeHelper.restartMainActivity(requireContext())
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View File

@ -4,7 +4,9 @@ import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.github.libretube.PIPED_FRONTEND_URL
import com.github.libretube.R
import com.github.libretube.YOUTUBE_FRONTEND_URL
import com.github.libretube.preferences.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -30,8 +32,8 @@ class ShareDialog(
shareOptions
) { _, which ->
val host = when (which) {
0 -> "https://piped.kavin.rocks"
1 -> "https://youtube.com"
0 -> PIPED_FRONTEND_URL
1 -> YOUTUBE_FRONTEND_URL
// only available for custom instances
else -> instanceUrl
}
@ -57,7 +59,7 @@ class ShareDialog(
val instancePref = PreferenceHelper.getString(
requireContext(),
"selectInstance",
"https://pipedapi.kavin.rocks"
PIPED_FRONTEND_URL
)
// get the api urls of the other custom instances

View File

@ -16,7 +16,6 @@ import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.formatShort
import com.google.android.material.button.MaterialButton
import com.squareup.picasso.Picasso
import retrofit2.HttpException
import java.io.IOException
@ -58,7 +57,7 @@ class ChannelFragment : Fragment() {
binding.channelRefresh.isRefreshing = true
fetchChannel()
if (PreferenceHelper.getToken(requireContext()) != "") {
isSubscribed(binding.channelSubscribe)
isSubscribed()
}
}
refreshChannel()
@ -81,7 +80,7 @@ class ChannelFragment : Fragment() {
}
}
private fun isSubscribed(button: MaterialButton) {
private fun isSubscribed() {
@SuppressLint("ResourceAsColor")
fun run() {
lifecycleScope.launchWhenCreated {
@ -91,28 +90,26 @@ class ChannelFragment : Fragment() {
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")
} catch (e: Exception) {
Log.e(TAG, e.toString())
return@launchWhenCreated
}
runOnUiThread {
if (response.subscribed == true) {
isSubscribed = true
button.text = getString(R.string.unsubscribe)
binding.channelSubscribe.text = getString(R.string.unsubscribe)
}
if (response.subscribed != null) {
button.setOnClickListener {
if (isSubscribed) {
unsubscribe()
button.text = getString(R.string.subscribe)
} else {
subscribe()
button.text = getString(R.string.unsubscribe)
binding.channelSubscribe.apply {
setOnClickListener {
text = if (isSubscribed) {
unsubscribe()
getString(R.string.subscribe)
} else {
subscribe()
getString(R.string.unsubscribe)
}
}
}
}
@ -125,19 +122,14 @@ class ChannelFragment : Fragment() {
private fun subscribe() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
try {
val token = PreferenceHelper.getToken(requireContext())
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
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
isSubscribed = true
}
@ -148,19 +140,14 @@ class ChannelFragment : Fragment() {
private fun unsubscribe() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
try {
val token = PreferenceHelper.getToken(requireContext())
RetrofitInstance.authApi.unsubscribe(
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")
return@launchWhenCreated
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
isSubscribed = false
}

View File

@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.adapters.PlaylistsAdapter
import com.github.libretube.databinding.FragmentLibraryBinding
@ -77,7 +78,7 @@ class LibraryFragment : Fragment() {
override fun onResume() {
// optimize CreatePlaylistFab bottom margin if miniPlayer active
val layoutParams = binding.createPlaylist.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.bottomMargin = if (isMiniPlayerVisible) 180 else 64
layoutParams.bottomMargin = if (Globals.isMiniPlayerVisible) 180 else 64
binding.createPlaylist.layoutParams = layoutParams
super.onResume()
}

View File

@ -6,6 +6,8 @@ import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect
import android.net.Uri
import android.os.Build
@ -31,6 +33,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.activities.hideKeyboard
@ -53,6 +56,7 @@ import com.github.libretube.obj.Streams
import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.services.IS_DOWNLOAD_RUNNING
import com.github.libretube.util.BackgroundMode
import com.github.libretube.util.CronetHelper
import com.github.libretube.util.DescriptionAdapter
import com.github.libretube.util.RetrofitInstance
@ -93,9 +97,6 @@ import java.io.IOException
import java.util.concurrent.Executors
import kotlin.math.abs
var isFullScreen = false
var isMiniPlayerVisible = false
class PlayerFragment : Fragment() {
private val TAG = "PlayerFragment"
@ -137,8 +138,11 @@ class PlayerFragment : Fragment() {
private lateinit var title: String
private lateinit var uploader: String
private lateinit var thumbnailUrl: String
private lateinit var chapters: List<ChapterSegment>
private val sponsorBlockPrefs = SponsorBlockPrefs()
private var autoRotationEnabled = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
@ -162,6 +166,34 @@ class PlayerFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
hideKeyboard()
// save whether auto rotation is enabled
autoRotationEnabled = PreferenceHelper.getBoolean(
requireContext(),
"auto_fullscreen",
false
)
val mainActivity = activity as MainActivity
if (autoRotationEnabled) {
// enable auto rotation
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
onConfigurationChanged(resources.configuration)
} else {
// go to portrait mode
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
// save whether related streams and autoplay are enabled
autoplay = PreferenceHelper.getBoolean(
requireContext(),
"autoplay",
false
)
relatedStreamsEnabled = PreferenceHelper.getBoolean(
requireContext(),
"related_streams_toggle",
true
)
setSponsorBlockPrefs()
createExoPlayer(view)
initializeTransitionLayout(view)
@ -205,11 +237,11 @@ class PlayerFragment : Fragment() {
val mainMotionLayout =
mainActivity.binding.mainMotionLayout
if (currentId == eId) {
isMiniPlayerVisible = true
Globals.isMiniPlayerVisible = true
exoPlayerView.useController = false
mainMotionLayout.progress = 1F
} else if (currentId == sId) {
isMiniPlayerVisible = false
Globals.isMiniPlayerVisible = false
exoPlayerView.useController = true
mainMotionLayout.progress = 0F
}
@ -228,19 +260,17 @@ class PlayerFragment : Fragment() {
binding.playerMotionLayout.transitionToStart()
binding.closeImageView.setOnClickListener {
isMiniPlayerVisible = false
Globals.isMiniPlayerVisible = false
binding.playerMotionLayout.transitionToEnd()
val mainActivity = activity as MainActivity
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
mainActivity.supportFragmentManager.beginTransaction()
.remove(this)
.commit()
}
playerBinding.closeImageButton.setOnClickListener {
isMiniPlayerVisible = false
Globals.isMiniPlayerVisible = false
binding.playerMotionLayout.transitionToEnd()
val mainActivity = activity as MainActivity
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
mainActivity.supportFragmentManager.beginTransaction()
.remove(this)
.commit()
@ -259,9 +289,7 @@ class PlayerFragment : Fragment() {
// video description and chapters toggle
binding.playerTitleLayout.setOnClickListener {
binding.playerDescriptionArrow.animate().rotationBy(180F).setDuration(250).start()
binding.descLinLayout.visibility =
if (binding.descLinLayout.isVisible) View.GONE else View.VISIBLE
toggleDescription()
}
binding.commentsToggle.setOnClickListener {
@ -269,10 +297,12 @@ class PlayerFragment : Fragment() {
}
// FullScreen button trigger
// hide fullscreen button if auto rotation enabled
playerBinding.fullscreen.visibility = if (autoRotationEnabled) View.GONE else View.VISIBLE
playerBinding.fullscreen.setOnClickListener {
// hide player controller
exoPlayerView.hideController()
if (!isFullScreen) {
if (!Globals.isFullScreen) {
// go to fullscreen mode
setFullscreen()
} else {
@ -340,27 +370,27 @@ class PlayerFragment : Fragment() {
val fullscreenOrientationPref = PreferenceHelper
.getString(requireContext(), "fullscreen_orientation", "ratio")
val scaleFactor = 1.3F
playerBinding.exoPlayPause.scaleX = scaleFactor
playerBinding.exoPlayPause.scaleY = scaleFactor
scaleControls(1.3F)
val orientation = when (fullscreenOrientationPref) {
"ratio" -> {
val videoSize = exoPlayer.videoSize
// probably a youtube shorts video
Log.e(TAG, videoSize.height.toString() + " " + videoSize.width.toString())
if (videoSize.height > videoSize.width) ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
// a video with normal aspect ratio
else ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
if (!autoRotationEnabled) {
// different orientations of the video are only available when auto rotation is disabled
val orientation = when (fullscreenOrientationPref) {
"ratio" -> {
val videoSize = exoPlayer.videoSize
// probably a youtube shorts video
if (videoSize.height > videoSize.width) ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
// a video with normal aspect ratio
else ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
"auto" -> ActivityInfo.SCREEN_ORIENTATION_USER
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
"auto" -> ActivityInfo.SCREEN_ORIENTATION_USER
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
mainActivity.requestedOrientation = orientation
}
mainActivity.requestedOrientation = orientation
isFullScreen = true
Globals.isFullScreen = true
}
private fun unsetFullscreen() {
@ -375,14 +405,26 @@ class PlayerFragment : Fragment() {
playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen)
playerBinding.exoTitle.visibility = View.INVISIBLE
val scaleFactor = 1F
scaleControls(1F)
if (!autoRotationEnabled) {
// switch back to portrait mode if auto rotation disabled
val mainActivity = activity as MainActivity
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
Globals.isFullScreen = false
}
private fun scaleControls(scaleFactor: Float) {
playerBinding.exoPlayPause.scaleX = scaleFactor
playerBinding.exoPlayPause.scaleY = scaleFactor
}
val mainActivity = activity as MainActivity
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
isFullScreen = false
private fun toggleDescription() {
binding.playerDescriptionArrow.animate().rotationBy(180F).setDuration(250).start()
binding.descLinLayout.visibility =
if (binding.descLinLayout.isVisible) View.GONE else View.VISIBLE
}
private fun toggleComments() {
@ -492,16 +534,12 @@ class PlayerFragment : Fragment() {
uploader = response.uploader!!
thumbnailUrl = response.thumbnailUrl!!
// save whether related streams and autoplay are enabled
autoplay = PreferenceHelper.getBoolean(requireContext(), "autoplay", false)
relatedStreamsEnabled =
PreferenceHelper.getBoolean(requireContext(), "related_streams_toggle", true)
// save related streams for autoplay
relatedStreams = response.relatedStreams
runOnUiThread {
// set media sources for the player
setResolutionAndSubtitles(view, response)
setResolutionAndSubtitles(response)
prepareExoPlayerView()
initializePlayerView(view, response)
seekToWatchPosition()
@ -682,7 +720,6 @@ class PlayerFragment : Fragment() {
setShowSubtitleButton(true)
setShowNextButton(false)
setShowPreviousButton(false)
setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL)
// controllerShowTimeoutMs = 1500
controllerHideOnTouch = true
useController = false
@ -709,7 +746,33 @@ class PlayerFragment : Fragment() {
enableDoubleTapToSeek()
// init the chapters recyclerview
if (response.chapters != null) initializeChapters(response.chapters)
if (response.chapters != null) {
chapters = response.chapters
initializeChapters()
}
// set default playback speed
val playbackSpeed =
PreferenceHelper.getString(requireContext(), "playback_speed", "1F")!!
val playbackSpeeds = context?.resources?.getStringArray(R.array.playbackSpeed)!!
val playbackSpeedValues =
context?.resources?.getStringArray(R.array.playbackSpeedValues)!!
exoPlayer.setPlaybackSpeed(playbackSpeed.toFloat())
val speedIndex = playbackSpeedValues.indexOf(playbackSpeed)
playerBinding.speedText.text = playbackSpeeds[speedIndex]
// change playback speed button
playerBinding.speedText.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.change_playback_speed)
.setItems(playbackSpeeds) { _, index ->
// set the new playback speed
val newPlaybackSpeed = playbackSpeedValues[index].toFloat()
exoPlayer.setPlaybackSpeed(newPlaybackSpeed)
playerBinding.speedText.text = playbackSpeeds[index]
}
.show()
}
// Listener for play and pause icon change
exoPlayer.addListener(object : Player.Listener {
@ -777,11 +840,37 @@ class PlayerFragment : Fragment() {
}
})
// repeat toggle button
playerBinding.repeatToggle.setOnClickListener {
if (exoPlayer.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) {
// turn off repeat mode
exoPlayer.repeatMode = RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
playerBinding.repeatToggle.setColorFilter(Color.GRAY)
} else {
exoPlayer.repeatMode = RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
playerBinding.repeatToggle.setColorFilter(Color.WHITE)
}
}
// share button
binding.relPlayerShare.setOnClickListener {
val shareDialog = ShareDialog(videoId!!, false)
shareDialog.show(childFragmentManager, "ShareDialog")
}
binding.relPlayerBackground.setOnClickListener {
// pause the current player
exoPlayer.pause()
// start the background mode
BackgroundMode
.getInstance()
.playOnBackgroundMode(
requireContext(),
videoId!!
)
}
// check if livestream
if (response.duration!! > 0) {
// download clicked
@ -850,7 +939,7 @@ class PlayerFragment : Fragment() {
if (token != "") {
val channelId = response.uploaderUrl?.replace("/channel/", "")
isSubscribed(binding.playerSubscribe, channelId!!)
binding.save.setOnClickListener {
binding.relPlayerSave.setOnClickListener {
val newFragment = AddtoPlaylistDialog()
val bundle = Bundle()
bundle.putString("videoId", videoId)
@ -903,6 +992,12 @@ class PlayerFragment : Fragment() {
)
}
private fun disableDoubleTapToSeek() {
// disable fast forward and rewind by double tapping
binding.forwardFL.visibility = View.GONE
binding.rewindFL.visibility = View.GONE
}
// toggle the visibility of the player controller
private fun toggleController() {
if (exoPlayerView.isControllerFullyVisible) exoPlayerView.hideController()
@ -917,10 +1012,15 @@ class PlayerFragment : Fragment() {
}
override fun onScrubMove(timeBar: TimeBar, position: Long) {
exoPlayer.seekTo(position)
val minTimeDiff = 10 * 1000 // 10s
// get the difference between the new and the old position
val diff = abs(exoPlayer.currentPosition - position)
// seek only when the difference is greater than 10 seconds
if (diff >= minTimeDiff) exoPlayer.seekTo(position)
}
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
exoPlayer.seekTo(position)
exoPlayer.play()
Handler(Looper.getMainLooper()).postDelayed({
exoPlayerView.hideController()
@ -929,15 +1029,69 @@ class PlayerFragment : Fragment() {
})
}
private fun initializeChapters(chapters: List<ChapterSegment>) {
private fun initializeChapters() {
if (chapters.isNotEmpty()) {
// enable chapters in the video description
binding.chaptersRecView.layoutManager =
LinearLayoutManager(this.context, LinearLayoutManager.HORIZONTAL, false)
LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.chaptersRecView.adapter = ChaptersAdapter(chapters, exoPlayer)
binding.chaptersRecView.visibility = View.VISIBLE
// enable the chapters dialog in the player
val titles = mutableListOf<String>()
chapters.forEach {
titles += it.title!!
}
playerBinding.chapterLL.visibility = View.VISIBLE
playerBinding.chapterLL.setOnClickListener {
if (Globals.isFullScreen) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chapters)
.setItems(titles.toTypedArray()) { _, index ->
val position = chapters[index].start!! * 1000
exoPlayer.seekTo(position)
}
.show()
} else {
toggleDescription()
}
}
setCurrentChapterName()
}
}
// set the name of the video chapter in the exoPlayerView
private fun setCurrentChapterName() {
// call the function again in 100ms
exoPlayerView.postDelayed(this::setCurrentChapterName, 100)
val chapterName = getCurrentChapterName()
// change the chapter name textView text to the chapterName
if (chapterName != null && chapterName != playerBinding.chapterName.text) {
playerBinding.chapterName.text = chapterName
}
}
// get the name of the currently played chapter
private fun getCurrentChapterName(): String? {
val currentPosition = exoPlayer.currentPosition
var chapterName: String? = null
chapters.forEach {
// check whether the chapter start is greater than the current player position
if (currentPosition >= it.start!! * 1000) {
// save chapter title if found
chapterName = it.title
}
}
return chapterName
}
private fun setMediaSource(
subtitle: MutableList<SubtitleConfiguration>,
videoUri: Uri,
@ -960,7 +1114,7 @@ class PlayerFragment : Fragment() {
exoPlayer.setMediaSource(mergeSource)
}
private fun setResolutionAndSubtitles(view: View, response: Streams) {
private fun setResolutionAndSubtitles(response: Streams) {
val videoFormatPreference =
PreferenceHelper.getString(requireContext(), "player_video_format", "WEBM")
val defres = PreferenceHelper.getString(requireContext(), "default_res", "")!!
@ -976,8 +1130,8 @@ class PlayerFragment : Fragment() {
for (vid in response.videoStreams!!) {
// append quality to list if it has the preferred format (e.g. MPEG)
if (vid.format.equals(videoFormatPreference)) { // preferred format
videosNameArray += vid.quality!!
if (vid.format.equals(videoFormatPreference) && vid.url != null) { // preferred format
videosNameArray += vid.quality.toString()
videosUrlArray += vid.url!!.toUri()
} else if (vid.quality.equals("LBRY") && vid.format.equals("MP4")) { // LBRY MP4 format)
videosNameArray += "LBRY MP4"
@ -1039,7 +1193,7 @@ class PlayerFragment : Fragment() {
}
}
playerBinding.qualityLinLayout.setOnClickListener {
playerBinding.qualityText.setOnClickListener {
// Dialog for quality selection
val builder: MaterialAlertDialogBuilder? = activity?.let {
MaterialAlertDialogBuilder(it)
@ -1074,13 +1228,8 @@ class PlayerFragment : Fragment() {
}
private fun createExoPlayer(view: View) {
val playbackSpeed =
PreferenceHelper.getString(requireContext(), "playback_speed", "1F")?.toFloat()
// multiply by thousand: s -> ms
val bufferingGoal =
PreferenceHelper.getString(requireContext(), "buffering_goal", "50")?.toInt()!! * 1000
val seekIncrement =
PreferenceHelper.getString(requireContext(), "seek_increment", "5")?.toLong()!! * 1000
val cronetEngine: CronetEngine = CronetHelper.getCronetEngine()
val cronetDataSourceFactory: CronetDataSource.Factory =
@ -1112,13 +1261,9 @@ class PlayerFragment : Fragment() {
exoPlayer = ExoPlayer.Builder(view.context)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.setLoadControl(loadControl)
.setSeekBackIncrementMs(seekIncrement)
.setSeekForwardIncrementMs(seekIncrement)
.build()
exoPlayer.setAudioAttributes(audioAttributes, true)
exoPlayer.setPlaybackSpeed(playbackSpeed!!)
}
private fun initializePlayerNotification(c: Context) {
@ -1145,13 +1290,18 @@ class PlayerFragment : Fragment() {
}
}
// lock the player
private fun lockPlayer(isLocked: Boolean) {
val visibility = if (isLocked) View.VISIBLE else View.GONE
playerBinding.exoTopBarRight.visibility = visibility
playerBinding.exoPlayPause.visibility = visibility
playerBinding.exoBottomBar.visibility = visibility
playerBinding.closeImageButton.visibility = visibility
playerBinding.exoTitle.visibility = visibility
// disable double tap to seek when the player is locked
if (isLocked) enableDoubleTapToSeek() else disableDoubleTapToSeek()
}
private fun isSubscribed(button: MaterialButton, channel_id: String) {
@ -1198,7 +1348,7 @@ class PlayerFragment : Fragment() {
private fun subscribe(channel_id: String) {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
try {
val token = PreferenceHelper.getToken(requireContext())
RetrofitInstance.authApi.subscribe(
token,
@ -1221,7 +1371,7 @@ class PlayerFragment : Fragment() {
private fun unsubscribe(channel_id: String) {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
try {
val token = PreferenceHelper.getToken(requireContext())
RetrofitInstance.authApi.unsubscribe(
token,
@ -1305,28 +1455,19 @@ class PlayerFragment : Fragment() {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (isInPictureInPictureMode) {
// hide and disable exoPlayer controls
exoPlayerView.hideController()
exoPlayerView.useController = false
binding.linLayout.visibility = View.GONE
with(binding.playerMotionLayout) {
getConstraintSet(R.id.start).constrainHeight(R.id.player, -1)
enableTransition(R.id.yt_transition, false)
}
binding.mainContainer.isClickable = true
unsetFullscreen()
val mainActivity = activity as MainActivity
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
isFullScreen = false
Globals.isFullScreen = false
} else {
with(binding.playerMotionLayout) {
getConstraintSet(R.id.start).constrainHeight(R.id.player, 0)
enableTransition(R.id.yt_transition, true)
}
// enable exoPlayer controls again
exoPlayerView.useController = true
binding.linLayout.visibility = View.VISIBLE
binding.mainContainer.isClickable = false
// switch back to portrait mode
unsetFullscreen()
}
}
@ -1335,7 +1476,7 @@ class PlayerFragment : Fragment() {
binding.playerScrollView.getHitRect(bounds)
if (SDK_INT >= Build.VERSION_CODES.O &&
exoPlayer.isPlaying && (binding.playerScrollView.getLocalVisibleRect(bounds) || isFullScreen)
exoPlayer.isPlaying && (binding.playerScrollView.getLocalVisibleRect(bounds) || Globals.isFullScreen)
) {
activity?.enterPictureInPictureMode(updatePipParams())
}
@ -1344,4 +1485,19 @@ class PlayerFragment : Fragment() {
private fun updatePipParams() = PictureInPictureParams.Builder()
.setActions(emptyList())
.build()
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (autoRotationEnabled) {
val orientation = newConfig.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
// go to fullscreen mode
setFullscreen()
} else {
// exit fullscreen if not landscape
unsetFullscreen()
}
}
}
}

View File

@ -6,7 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
data class ChapterSegment(
var title: String?,
var image: String?,
var start: Int?
var start: Long?
) {
constructor() : this("", "", -1)
}

View File

@ -0,0 +1,11 @@
package com.github.libretube.obj
/**
* object for saving the download type
*/
object DownloadType {
const val AUDIO = 0
const val VIDEO = 1
const val MUX = 2
const val NONE = 3
}

View File

@ -5,7 +5,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.dialogs.RequireRestartDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class AdvancedSettings : PreferenceFragmentCompat() {
@ -48,8 +48,8 @@ class AdvancedSettings : PreferenceFragmentCompat() {
// clear login token
PreferenceHelper.setToken(requireContext(), "")
requireMainActivityRestart = true
activity?.recreate()
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
}
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
.setTitle(R.string.reset)

View File

@ -6,7 +6,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.dialogs.RequireRestartDialog
import com.github.libretube.util.ThemeHelper
import com.google.android.material.color.DynamicColors
@ -18,18 +18,18 @@ class AppearanceSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.appearance))
val themeToggle = findPreference<ListPreference>("theme_togglee")
val themeToggle = findPreference<ListPreference>("theme_toggle")
themeToggle?.setOnPreferenceChangeListener { _, _ ->
requireMainActivityRestart = true
activity?.recreate()
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val accentColor = findPreference<ListPreference>("accent_color")
updateAccentColorValues(accentColor!!)
accentColor.setOnPreferenceChangeListener { _, _ ->
requireMainActivityRestart = true
activity?.recreate()
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
@ -41,13 +41,22 @@ class AppearanceSettings : PreferenceFragmentCompat() {
val gridColumns = findPreference<ListPreference>("grid")
gridColumns?.setOnPreferenceChangeListener { _, _ ->
requireMainActivityRestart = true
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val hideTrending = findPreference<SwitchPreference>("hide_trending_page")
hideTrending?.setOnPreferenceChangeListener { _, _ ->
requireMainActivityRestart = true
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val labelVisibilityMode = findPreference<ListPreference>("label_visibility")
labelVisibilityMode?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
}

View File

@ -21,11 +21,11 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.dialogs.CustomInstanceDialog
import com.github.libretube.dialogs.DeleteAccountDialog
import com.github.libretube.dialogs.LoginDialog
import com.github.libretube.dialogs.LogoutDialog
import com.github.libretube.dialogs.RequireRestartDialog
import com.github.libretube.util.RetrofitInstance
import org.json.JSONObject
import org.json.JSONTokener
@ -119,7 +119,8 @@ class InstanceSettings : PreferenceFragmentCompat() {
// fetchInstance()
initCustomInstances(instance!!)
instance.setOnPreferenceChangeListener { _, newValue ->
requireMainActivityRestart = true
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
RetrofitInstance.url = newValue.toString()
if (!PreferenceHelper.getBoolean(requireContext(), "auth_instance_toggle", false)) {
RetrofitInstance.authUrl = newValue.toString()
@ -136,22 +137,24 @@ class InstanceSettings : PreferenceFragmentCompat() {
authInstance.isVisible = false
}
authInstance.setOnPreferenceChangeListener { _, newValue ->
requireMainActivityRestart = true
// save new auth url
RetrofitInstance.authUrl = newValue.toString()
RetrofitInstance.lazyMgr.reset()
logout()
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val authInstanceToggle = findPreference<SwitchPreference>("auth_instance_toggle")
authInstanceToggle?.setOnPreferenceChangeListener { _, newValue ->
requireMainActivityRestart = true
authInstance.isVisible = newValue == true
logout()
// either use new auth url or the normal api url if auth instance disabled
RetrofitInstance.authUrl = if (newValue == false) RetrofitInstance.url
else authInstance.value
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}

View File

@ -7,9 +7,9 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.BuildConfig
import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.activities.isCurrentViewMainSettings
import com.github.libretube.activities.requireMainActivityRestart
import com.github.libretube.dialogs.RequireRestartDialog
import com.github.libretube.util.ThemeHelper
import com.github.libretube.util.checkUpdate
@ -25,7 +25,8 @@ class MainSettings : PreferenceFragmentCompat() {
val region = findPreference<Preference>("region")
region?.setOnPreferenceChangeListener { _, _ ->
requireMainActivityRestart = true
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
@ -93,7 +94,7 @@ class MainSettings : PreferenceFragmentCompat() {
}
private fun navigateToSettingsFragment(newFragment: Fragment) {
isCurrentViewMainSettings = false
Globals.isCurrentViewMainSettings = false
parentFragmentManager.beginTransaction()
.replace(R.id.settings, newFragment)
.commitNow()

View File

@ -1,7 +1,9 @@
package com.github.libretube.preferences
import android.os.Bundle
import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
@ -13,5 +15,20 @@ class PlayerSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.audio_video))
val playerOrientation = findPreference<ListPreference>("fullscreen_orientation")
val autoRotateToFullscreen = findPreference<SwitchPreferenceCompat>("auto_fullscreen")
// only show the player orientation option if auto fullscreen is disabled
playerOrientation?.isEnabled != PreferenceHelper.getBoolean(
requireContext(),
"auto_fullscreen",
false
)
autoRotateToFullscreen?.setOnPreferenceChangeListener { _, newValue ->
playerOrientation?.isEnabled = newValue != true
true
}
}
}

View File

@ -19,6 +19,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.arthenica.ffmpegkit.FFmpegKit
import com.github.libretube.R
import com.github.libretube.obj.DownloadType
import com.github.libretube.preferences.PreferenceHelper
import java.io.File
@ -26,17 +27,19 @@ var IS_DOWNLOAD_RUNNING = false
class DownloadService : Service() {
val TAG = "DownloadService"
private lateinit var notification: NotificationCompat.Builder
private var downloadId: Long = -1
private lateinit var videoId: 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
private lateinit var videoDir: File
private lateinit var notification: NotificationCompat.Builder
private lateinit var downloadType: String
private lateinit var libretubeDir: File
private lateinit var tempDir: File
override fun onCreate() {
@ -50,11 +53,11 @@ class DownloadService : Service() {
audioUrl = intent.getStringExtra("audioUrl")!!
duration = intent.getIntExtra("duration", 1)
extension = PreferenceHelper.getString(this, "video_format", ".mp4")!!
downloadType = if (audioUrl != "" && videoUrl != "") "mux"
else if (audioUrl != "") "audio"
else if (videoUrl != "") "video"
else "none"
if (downloadType != "none") {
downloadType = if (audioUrl != "" && videoUrl != "") DownloadType.MUX
else if (audioUrl != "") DownloadType.AUDIO
else if (videoUrl != "") DownloadType.VIDEO
else DownloadType.NONE
if (downloadType != DownloadType.NONE) {
downloadNotification(intent)
downloadManager()
} else {
@ -108,7 +111,7 @@ class DownloadService : Service() {
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
when (downloadType) {
"mux" -> {
DownloadType.MUX -> {
audioDir = File(tempDir, "$videoId-audio")
videoDir = File(tempDir, "$videoId-video")
downloadId = downloadManagerRequest(
@ -118,7 +121,7 @@ class DownloadService : Service() {
videoDir
)
}
"video" -> {
DownloadType.VIDEO -> {
videoDir = File(libretubeDir, "$videoId-video")
downloadId = downloadManagerRequest(
getString(R.string.video),
@ -127,7 +130,7 @@ class DownloadService : Service() {
videoDir
)
}
"audio" -> {
DownloadType.AUDIO -> {
audioDir = File(libretubeDir, "$videoId-audio")
downloadId = downloadManagerRequest(
getString(R.string.audio),
@ -148,7 +151,7 @@ class DownloadService : Service() {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
// Checking if the received broadcast is for our enqueued download by matching download id
if (downloadId == id) {
if (downloadType == "mux") {
if (downloadType == DownloadType.MUX) {
downloadManagerRequest(
getString(R.string.audio),
getString(R.string.downloading),

View File

@ -2,13 +2,14 @@ package com.github.libretube.util
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
object ConnectionHelper {
fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
// this seems to not recognize vpn connections
/*
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val nw = connectivityManager.activeNetwork ?: return false
val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false
@ -28,5 +29,8 @@ object ConnectionHelper {
} else {
return connectivityManager.activeNetworkInfo?.isConnected ?: false
}
*/
return connectivityManager.activeNetworkInfo?.isConnected ?: false
}
}

View File

@ -12,35 +12,60 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.text.HtmlCompat
import com.github.libretube.R
import com.github.libretube.preferences.PreferenceHelper
import com.google.android.material.color.DynamicColors
object ThemeHelper {
fun updateTheme(context: Context) {
updateAccentColor(context)
updateThemeMode(context)
fun updateTheme(activity: AppCompatActivity) {
val themeMode = PreferenceHelper.getString(activity, "theme_toggle", "A")!!
val blackModeEnabled = themeMode == "O"
updateAccentColor(activity, blackModeEnabled)
updateThemeMode(themeMode)
}
private fun updateAccentColor(context: Context) {
when (PreferenceHelper.getString(context, "accent_color", "purple")) {
"my" -> context.setTheme(R.style.MaterialYou)
"red" -> context.setTheme(R.style.Theme_Red)
"blue" -> context.setTheme(R.style.Theme_Blue)
"yellow" -> context.setTheme(R.style.Theme_Yellow)
"green" -> context.setTheme(R.style.Theme_Green)
"purple" -> context.setTheme(R.style.Theme_Purple)
}
}
private fun updateThemeMode(context: Context) {
when (PreferenceHelper.getString(context, "theme_togglee", "A")) {
"A" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"L" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"D" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"O" -> {
context.setTheme(R.style.OLED)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
private fun updateAccentColor(
activity: AppCompatActivity,
blackThemeEnabled: Boolean
) {
val theme = when (
PreferenceHelper.getString(
activity,
"accent_color",
"purple"
)
) {
"my" -> {
applyDynamicColors(activity)
if (blackThemeEnabled) R.style.MaterialYou_Black
else R.style.MaterialYou
}
"red" -> if (blackThemeEnabled) R.style.Theme_Red_Black else R.style.Theme_Red
"blue" -> if (blackThemeEnabled) R.style.Theme_Blue_Black else R.style.Theme_Blue
"yellow" -> if (blackThemeEnabled) R.style.Theme_Yellow_Black else R.style.Theme_Yellow
"green" -> if (blackThemeEnabled) R.style.Theme_Green_Black else R.style.Theme_Green
"purple" -> if (blackThemeEnabled) R.style.Theme_Purple_Black else R.style.Theme_Purple
else -> if (blackThemeEnabled) R.style.Theme_Purple_Black else R.style.Theme_Purple
}
activity.setTheme(theme)
}
private fun applyDynamicColors(activity: AppCompatActivity) {
/**
* apply dynamic colors to the activity
*/
DynamicColors.applyToActivityIfAvailable(activity)
}
private fun updateThemeMode(themeMode: String) {
val mode = when (themeMode) {
"A" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
"L" -> AppCompatDelegate.MODE_NIGHT_NO
"D" -> AppCompatDelegate.MODE_NIGHT_YES
"O" -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(mode)
}
fun changeIcon(context: Context, newLogoActivityAlias: String) {

View File

@ -5,32 +5,34 @@ import android.os.Looper
import android.view.View
class DoubleClickListener(
private val doubleClickTimeLimitMills: Long = 300,
private val doubleClickTimeLimitMills: Long = 200,
private val callback: Callback
) : View.OnClickListener {
private var lastClicked: Long = -1L
private var doubleClicked: Boolean = false
override fun onClick(v: View?) {
lastClicked = when {
lastClicked == -1L -> {
doubleClicked = false
checkForSingleClick()
System.currentTimeMillis()
}
isDoubleClicked() -> {
doubleClicked = true
callback.doubleClicked()
-1L
}
else -> {
Handler(Looper.getMainLooper()).postDelayed({
if (!doubleClicked) callback.singleClicked()
}, doubleClickTimeLimitMills)
checkForSingleClick()
System.currentTimeMillis()
}
}
}
private fun checkForSingleClick() {
Handler(Looper.getMainLooper()).postDelayed({
if (lastClicked != -1L) callback.singleClicked()
}, doubleClickTimeLimitMills)
}
private fun getTimeDiff(from: Long, to: Long): Long {
return to - from
}

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z" />
</vector>

View File

@ -2,13 +2,9 @@
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="36"
android:viewportHeight="36">
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF000000"
android:pathData="M23.38,16.77l0.6,-0.6A5,5 0,0 0,24 9.1L18.71,3.84a5,5 0,0 0,-7.07 0L3.09,12.39a5,5 0,0 0,0 7.07l5.26,5.26a5,5 0,0 0,7.07 0l0.45,-0.45 2.1,2.2h3.44v3h3.69v1.63L28,34h6L34,27.45ZM14.82,10.18L9.37,15.64a1,1 0,0 1,-1.41 0l-0.4,-0.4a1,1 0,0 1,0 -1.41L13,8.36a1,1 0,0 1,1.41 0l0.4,0.4A1,1 0,0 1,14.82 10.18ZM32,32L28.86,32l-1.77,-1.76v-2.8L23.41,27.44v-3L18.8,24.44l-1.52,-1.61L22,18.18 32,28.28Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M0,0h36v36h-36z" />
android:pathData="M14,30.45q2.7,0 4.575,-1.875T20.45,24q0,-2.7 -1.875,-4.575T14,17.55q-2.7,0 -4.575,1.875T7.55,24q0,2.7 1.875,4.575T14,30.45ZM14,36q-5,0 -8.5,-3.5T2,24q0,-5 3.5,-8.5T14,12q4.25,0 7.125,2.325t4.025,5.925h17.2L46,23.9l-7.3,7.3 -4.2,-4.2 -4.2,4.2 -3.55,-3.55h-1.6q-0.95,3.75 -4.025,6.05T14,36Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF000000"
android:pathData="M32.9,21.5 L28.4,26l1.45,1.8 1.85,-1.85q-0.35,3.3 -2.45,5.6T24,33.85h-1.35q-0.55,0 -1.25,-0.25l-1.9,1.55q1,0.45 2.125,0.825 1.125,0.375 2.375,0.375 4.15,0 7.05,-3 2.9,-3 3.15,-7.85l2.1,2.3 1.45,-1.8ZM15.1,30.85 L19.6,26.35 18.15,24.55 16.3,26.4q0.35,-3.3 2.45,-5.6T24,18.5h1.35q0.55,0 1.25,0.25l1.9,-1.55q-1,-0.45 -2.125,-0.825Q25.25,16 24,16q-4.15,0 -7.05,3 -2.9,3 -3.15,7.85l-2.1,-2.3 -1.45,1.8ZM7,42q-1.2,0 -2.1,-0.9Q4,40.2 4,39L4,13.35q0,-1.15 0.9,-2.075 0.9,-0.925 2.1,-0.925h7.35L18,6h12l3.65,4.35L41,10.35q1.15,0 2.075,0.925Q44,12.2 44,13.35L44,39q0,1.2 -0.925,2.1 -0.925,0.9 -2.075,0.9Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF000000"
android:pathData="M16.4,42H9q-1.2,0 -2.1,-0.9Q6,40.2 6,39V24q0,-3.75 1.425,-7.025 1.425,-3.275 3.85,-5.7 2.425,-2.425 5.7,-3.85Q20.25,6 24,6q3.75,0 7.025,1.425 3.275,1.425 5.7,3.85 2.425,2.425 3.85,5.7Q42,20.25 42,24v15q0,1.2 -0.9,2.1 -0.9,0.9 -2.1,0.9h-7.4V27.2H39V24q0,-6.25 -4.375,-10.625T24,9q-6.25,0 -10.625,4.375T9,24v3.2h7.4Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF000000"
android:pathData="m42,24 l-8.45,11.95q-0.65,0.9 -1.55,1.475 -0.9,0.575 -2,0.575H9q-1.25,0 -2.125,-0.875T6,35V13q0,-1.25 0.875,-2.125T9,10h21q1.1,0 2,0.575 0.9,0.575 1.55,1.475Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF000000"
android:pathData="m7,8 l3.7,7.6h6.5L13.5,8h4.45l3.7,7.6h6.5L24.45,8h4.45l3.7,7.6h6.5L35.4,8H41q1.2,0 2.1,0.9 0.9,0.9 0.9,2.1v26q0,1.2 -0.9,2.1 -0.9,0.9 -2.1,0.9H7q-1.2,0 -2.1,-0.9Q4,38.2 4,37V11q0,-1.2 0.9,-2.1Q5.8,8 7,8Z" />
</vector>

View File

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="244.86"
android:viewportHeight="244.86">
<path
android:fillColor="#FF000000"
android:pathData="M240.9,38.89c-2.43,-1.31 -5.39,-1.17 -7.69,0.35l-64.63,42.81V64.38c0,-14.48 -11.78,-26.25 -26.25,-26.25H26.25C11.78,38.13 0,49.91 0,64.38v116.09c0,14.47 11.78,26.25 26.25,26.25h116.09c14.47,0 26.25,-11.78 26.25,-26.25v-17.67l64.63,42.81c1.25,0.83 2.69,1.25 4.14,1.25c1.22,0 2.44,-0.3 3.55,-0.89c2.43,-1.31 3.95,-3.85 3.95,-6.61V45.5C244.86,42.74 243.34,40.2 240.9,38.89zM153.59,180.47c0,6.2 -5.05,11.25 -11.25,11.25H26.25c-6.2,0 -11.25,-5.05 -11.25,-11.25V64.38c0,-6.2 5.05,-11.25 11.25,-11.25h116.09c6.2,0 11.25,5.05 11.25,11.25v31.64v52.82V180.47zM229.86,185.39l-61.27,-40.58v-44.76l61.27,-40.58V185.39z"
android:strokeWidth="10"
android:strokeColor="#000000" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z" />
</vector>

View File

@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="48"
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="m24.1,38 l5.7,-5.65 -5.7,-5.65 -2.1,2.1 2.15,2.15q-1.4,0.05 -2.725,-0.45 -1.325,-0.5 -2.375,-1.55 -1,-1 -1.525,-2.3 -0.525,-1.3 -0.525,-2.6 0,-0.85 0.225,-1.7t0.625,-1.65l-2.2,-2.2q-0.85,1.25 -1.25,2.65T14,24q0,1.9 0.75,3.75t2.2,3.3q1.45,1.45 3.25,2.175 1.8,0.725 3.7,0.775L22,35.9ZM32.35,29.5q0.85,-1.25 1.25,-2.65T34,24q0,-1.9 -0.725,-3.775T31.1,16.9q-1.45,-1.45 -3.275,-2.15t-3.725,-0.7L26,12.1 23.9,10l-5.7,5.65 5.7,5.65 2.1,-2.1 -2.2,-2.2q1.35,0 2.75,0.525t2.4,1.525q1,1 1.525,2.3 0.525,1.3 0.525,2.6 0,0.85 -0.225,1.7t-0.625,1.65ZM24,44q-4.1,0 -7.75,-1.575 -3.65,-1.575 -6.375,-4.3 -2.725,-2.725 -4.3,-6.375Q4,28.1 4,24q0,-4.15 1.575,-7.8 1.575,-3.65 4.3,-6.35 2.725,-2.7 6.375,-4.275Q19.9,4 24,4q4.15,0 7.8,1.575 3.65,1.575 6.35,4.275 2.7,2.7 4.275,6.35Q44,19.85 44,24q0,4.1 -1.575,7.75 -1.575,3.65 -4.275,6.375t-6.35,4.3Q28.15,44 24,44Z"/>
</vector>

View File

@ -2,12 +2,9 @@
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="28"
android:viewportHeight="28">
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#212121"
android:pathData="M5.25,5.5C3.455,5.5 2,6.955 2,8.75V19.25C2,21.045 3.455,22.5 5.25,22.5H14.75C16.545,22.5 18,21.045 18,19.25V8.75C18,6.955 16.545,5.5 14.75,5.5H5.25Z" />
<path
android:fillColor="#212121"
android:pathData="M23.123,20.643L19.5,17.094V10.999L23.112,7.371C23.899,6.58 25.248,7.138 25.248,8.253V19.75C25.248,20.858 23.914,21.418 23.123,20.643Z" />
android:fillColor="#FF000000"
android:pathData="M7,40q-1.2,0 -2.1,-0.9Q4,38.2 4,37V11q0,-1.2 0.9,-2.1Q5.8,8 7,8h26q1.2,0 2.1,0.9 0.9,0.9 0.9,2.1v10.75l8,-8v20.5l-8,-8V37q0,1.2 -0.9,2.1 -0.9,0.9 -2.1,0.9Z" />
</vector>

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/activity_main_scene"
tools:context=".MainActivity">
tools:context=".activities.MainActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"

View File

@ -1,34 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="?android:attr/selectableItemBackground">
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/search_channel_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="36dp"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="45dp"
android:layout_marginEnd="45dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/constraintLayout"
android:layout_width="wrap_content"
android:id="@+id/search_channel_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/search_channel_image"
app:layout_constraintTop_toTopOf="parent">
android:layout_gravity="center"
android:layout_marginEnd="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/search_channel_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp" />
@ -37,6 +36,15 @@
android:id="@+id/search_views"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/search_sub_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/subscribe"
android:textColor="?attr/colorPrimary"
android:visibility="gone" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -33,9 +33,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:backgroundTint="?attr/colorOnPrimary"
android:text="@string/unsubscribe"
android:textColor="@android:color/white"
android:textSize="11sp"
android:layout_centerVertical="true"
android:text="@string/subscribe"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
app:cornerRadius="20dp" />
</RelativeLayout>

View File

@ -9,6 +9,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
@ -23,8 +24,8 @@
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/commentor_image"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
app:srcCompat="@mipmap/ic_launcher" />
@ -52,7 +53,7 @@
android:id="@+id/verified_imageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_verified" />
@ -61,7 +62,7 @@
android:id="@+id/pinned_imageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_pinned" />
@ -85,7 +86,7 @@
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="6dp"
android:layout_marginEnd="6dp"
app:srcCompat="@drawable/ic_thumb_up" />
<TextView
@ -98,7 +99,7 @@
android:id="@+id/hearted_imageView"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="3dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_hearted" />
@ -110,6 +111,7 @@
android:id="@+id/replies_recView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:background="@null"
android:nestedScrollingEnabled="false" />

View File

@ -30,7 +30,7 @@
android:id="@+id/close_imageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:layout_marginEnd="5dp"
android:background="#00FFFFFF"
android:padding="@dimen/exo_icon_padding"
android:src="@drawable/ic_close"
@ -74,95 +74,122 @@
android:id="@+id/aspect_ratio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="#00FFFFFF"
android:layout_marginHorizontal="5dp"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/exo_icon_padding"
android:src="@drawable/ic_aspect_ratio" />
<LinearLayout
android:id="@+id/quality_linLayout"
<TextView
android:id="@+id/speed_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="5dp"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/exo_icon_padding"
android:text="1x"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/quality_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/exo_icon_padding"
android:text="@string/hls"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/quality_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="5dp"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/exo_icon_padding"
android:text="@string/hls"
android:textColor="#FFFFFF" />
<ImageView
android:id="@+id/quality_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/exo_icon_padding"
android:src="@drawable/ic_arrow_down"
app:tint="@android:color/white" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<FrameLayout
<LinearLayout
android:id="@id/exo_bottom_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_styled_bottom_bar_height"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top"
android:layoutDirection="ltr">
android:orientation="vertical">
<LinearLayout
android:id="@id/exo_time"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:layout_marginBottom="10dp"
android:layoutDirection="ltr"
android:layout_marginBottom="-5dp"
android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding">
<TextView
android:id="@id/exo_position"
style="@style/ExoStyledControls.TimeText.Position" />
<LinearLayout
android:id="@id/exo_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="5dp">
<TextView style="@style/ExoStyledControls.TimeText.Separator" />
<TextView
android:id="@id/exo_position"
style="@style/ExoStyledControls.TimeText.Position" />
<TextView
android:id="@id/exo_duration"
style="@style/ExoStyledControls.TimeText.Duration" />
<TextView style="@style/ExoStyledControls.TimeText.Separator" />
</LinearLayout>
<TextView
android:id="@id/exo_duration"
style="@style/ExoStyledControls.TimeText.Duration" />
<LinearLayout
android:id="@id/exo_basic_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginBottom="10dp"
android:layoutDirection="ltr">
</LinearLayout>
<ImageButton
android:id="@id/exo_repeat_toggle"
style="@style/ExoStyledControls.Button.Bottom.RepeatToggle" />
<LinearLayout
android:id="@+id/chapterLL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:visibility="invisible">
<ImageButton
android:id="@id/exo_subtitle"
style="@style/ExoStyledControls.Button.Bottom.CC" />
<TextView
android:id="@+id/chapter_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white" />
<ImageButton
android:id="@id/exo_settings"
style="@style/ExoStyledControls.Button.Bottom.Settings" />
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_gravity="center"
android:layout_marginStart="3dp"
android:src="@drawable/ic_arrow_right" />
<ImageButton
android:id="@+id/fullscreen"
style="@style/ExoStyledControls.Button.Bottom.FullScreen"
android:src="@drawable/ic_fullscreen"
app:tint="@android:color/white" />
</LinearLayout>
<LinearLayout
android:id="@id/exo_basic_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/repeat_toggle"
style="@style/PlayerButton"
android:src="@drawable/ic_repeat"
app:tint="@android:color/darker_gray" />
<ImageButton
android:id="@id/exo_subtitle"
style="@style/PlayerButton" />
<ImageButton
android:id="@+id/fullscreen"
style="@style/PlayerButton"
android:src="@drawable/ic_fullscreen"
app:tint="@android:color/white" />
</LinearLayout>
</LinearLayout>
@ -187,18 +214,6 @@
</LinearLayout>
</FrameLayout>
<LinearLayout
android:id="@id/exo_minimal_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom"
android:gravity="center_vertical"
android:layoutDirection="ltr"
android:orientation="horizontal">
</LinearLayout>
<LinearLayout

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.github.libretube.views.CustomSwipeToRefresh xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/channel_refresh"
android:layout_width="match_parent"
@ -74,15 +75,15 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/channel_subscribe"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:backgroundTint="?attr/colorOnPrimary"
android:drawableStart="@drawable/ic_bell_small"
android:drawableTint="@android:color/white"
android:drawableLeft="@drawable/ic_bell_small"
android:drawableTint="?android:attr/textColorPrimary"
android:text="@string/subscribe"
android:textColor="?android:attr/textColorPrimary"
android:textSize="11sp" />
android:textSize="12sp"
app:cornerRadius="20dp" />
</LinearLayout>

View File

@ -49,7 +49,7 @@
android:id="@+id/player_description_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_centerInParent="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
@ -73,7 +73,7 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginStart="3dp"
android:layout_marginTop="2dp">
<ImageView
@ -93,7 +93,7 @@
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:rotation="180"
android:src="@drawable/ic_like" />
@ -119,7 +119,6 @@
android:id="@+id/chapters_recView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/comments_toggle"
android:layout_marginHorizontal="3dp"
android:layout_marginTop="20dp"
android:nestedScrollingEnabled="false"
@ -193,7 +192,7 @@
android:layout_width="24dp"
android:layout_height="25dp"
android:padding="2dp"
android:src="@drawable/ic_player" />
android:src="@drawable/ic_videocam" />
<TextView
android:layout_width="wrap_content"
@ -203,7 +202,22 @@
</LinearLayout>
<LinearLayout
android:id="@+id/save"
android:id="@+id/relPlayer_background"
style="@style/PlayerActionsLayout">
<ImageView
android:layout_width="24dp"
android:layout_height="25dp"
android:src="@drawable/ic_headphones" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/audio" />
</LinearLayout>
<LinearLayout
android:id="@+id/relPlayer_save"
style="@style/PlayerActionsLayout">
<ImageView
@ -245,7 +259,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_toStartOf="@+id/player_subscribe"
android:layout_toEndOf="@+id/player_channelImage"
android:ellipsize="end"
@ -265,7 +279,7 @@
android:drawableTint="?android:attr/textColorPrimary"
android:text="@string/subscribe"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12dp"
android:textSize="12sp"
app:cornerRadius="11dp" />
</RelativeLayout>
@ -297,7 +311,7 @@
android:id="@+id/commentsToggle_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:text="@string/comments"
android:textSize="17sp"
app:layout_constraintStart_toStartOf="parent"
@ -307,7 +321,7 @@
android:id="@+id/commentsToggle_imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="3dp"
android:layout_marginEnd="3dp"
android:src="@drawable/ic_arrow_up_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -357,49 +371,64 @@
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:background="@android:color/black"
app:layout_constraintBottom_toBottomOf="@id/main_container"
app:layout_constraintStart_toStartOf="@id/main_container"
app:layout_constraintTop_toTopOf="@id/main_container"
app:show_buffering="when_playing">
<FrameLayout
android:id="@+id/forwardFL"
android:layout_width="wrap_content"
<!-- double tap to rewind/forward overlay -->
<LinearLayout
android:id="@+id/doubleTapOverlayLL"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|end"
android:layout_marginVertical="50dp">
<ImageButton
android:id="@+id/forwardBTN"
android:layout_width="150dp"
<!-- double tap rewind btn -->
<FrameLayout
android:id="@+id/rewindFL"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="?android:selectableItemBackgroundBorderless"
android:clickable="false"
android:src="@drawable/ic_forward"
android:visibility="invisible"
app:tint="@android:color/white" />
android:layout_weight=".35">
</FrameLayout>
<ImageButton
android:id="@+id/rewindBTN"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:selectableItemBackgroundBorderless"
android:clickable="false"
android:src="@drawable/ic_rewind"
android:visibility="invisible"
app:tint="@android:color/white" />
<FrameLayout
android:id="@+id/rewindFL"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|start"
android:layout_marginVertical="50dp">
</FrameLayout>
<ImageButton
android:id="@+id/rewindBTN"
android:layout_width="150dp"
<!-- place holder for the center controls -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="?android:selectableItemBackgroundBorderless"
android:clickable="false"
android:src="@drawable/ic_rewind"
android:visibility="invisible"
app:tint="@android:color/white" />
android:layout_weight=".30" />
</FrameLayout>
<!-- double tap forward btn -->
<FrameLayout
android:id="@+id/forwardFL"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".35">
<ImageButton
android:id="@+id/forwardBTN"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:selectableItemBackgroundBorderless"
android:clickable="false"
android:src="@drawable/ic_forward"
android:visibility="invisible"
app:tint="@android:color/white" />
</FrameLayout>
</LinearLayout>
</com.github.libretube.views.CustomExoPlayerView>

View File

@ -42,7 +42,7 @@
android:id="@+id/verified_imageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_verified" />
@ -51,7 +51,7 @@
android:id="@+id/pinned_imageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_pinned" />
@ -75,7 +75,7 @@
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="6dp"
android:layout_marginEnd="6dp"
app:srcCompat="@drawable/ic_thumb_up" />
<TextView
@ -88,7 +88,7 @@
android:id="@+id/hearted_imageView"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="3dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_hearted" />

View File

@ -227,4 +227,6 @@
<string name="telegram">تيليجرام</string>
<string name="reddit">ريديت</string>
<string name="twitter">تويتر</string>
<string name="turnInternetOn">يرجى الاتصال بالإنترنت عن طريق تشغيل WiFi أو بيانات الجوال.</string>
<string name="open">فتح …</string>
</resources>

View File

@ -229,4 +229,5 @@
<string name="discord">Discord</string>
<string name="open">ıq…</string>
<string name="turnInternetOn">Zəhmət olmasa, WiFi və ya mobil datanı yandırmaqla internetə qoşulun.</string>
<string name="chapters">Bölmələr</string>
</resources>

View File

@ -227,4 +227,7 @@
<string name="community">Comunidad</string>
<string name="discord">Discord</string>
<string name="twitter">Twitter</string>
<string name="turnInternetOn">Por favor, conéctese a Internet activando el WiFi o los datos móviles.</string>
<string name="open">Abrir…</string>
<string name="chapters">Capítulos</string>
</resources>

View File

@ -227,4 +227,7 @@
<string name="telegram">Telegram</string>
<string name="community">Communauté</string>
<string name="discord">Discord</string>
<string name="open">Ouvrir …</string>
<string name="turnInternetOn">Veuillez vous connecter à l\'internet en activant le WiFi ou les données mobiles.</string>
<string name="chapters">Chapitres</string>
</resources>

View File

@ -227,4 +227,6 @@
<string name="matrix">Matrix</string>
<string name="telegram">Telegram</string>
<string name="reddit">Reddit</string>
<string name="turnInternetOn">Connettiti a Internet attivando Wi-Fi o dati mobili.</string>
<string name="open">Apri …</string>
</resources>

View File

@ -227,4 +227,8 @@
<string name="portrait">לאורך</string>
<string name="aspect_ratio">יחס תצוגת וידאו</string>
<string name="twitter">טוויטר</string>
<string name="turnInternetOn">נא להתחבר לאינטרנט על ידי הפעלת הרשת האלחוטית או הנתונים הסלולריים.</string>
<string name="open">פתיחה…</string>
<string name="chapters">פרקים</string>
<string name="change_playback_speed">מהירות נגינה</string>
</resources>

View File

@ -8,6 +8,13 @@
</style>
<style name="MaterialYou.Black" parent="MaterialYou">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
</style>
<style name="Theme.Red" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/red_dark_accentLight</item>
@ -26,6 +33,13 @@
</style>
<style name="Theme.Red.Black" parent="Theme.Red">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
</style>
<style name="Theme.Blue" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/blue_dark_accentLight</item>
@ -44,6 +58,13 @@
</style>
<style name="Theme.Blue.Black" parent="Theme.Blue">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
</style>
<style name="Theme.Yellow" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/yellow_dark_accentLight</item>
@ -62,6 +83,13 @@
</style>
<style name="Theme.Yellow.Black" parent="Theme.Yellow">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
</style>
<style name="Theme.Green" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/green_dark_accentLight</item>
@ -80,6 +108,13 @@
</style>
<style name="Theme.Green.Black" parent="Theme.Green">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
</style>
<style name="Theme.Purple" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/purple_dark_accentLight</item>
@ -98,4 +133,11 @@
</style>
<style name="Theme.Purple.Black" parent="Theme.Purple">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
</style>
</resources>

View File

@ -1,58 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="loggedout">Desconectado com sucesso!</string>
<string name="loggedout">Sessão terminada.</string>
<string name="choose_quality_dialog">Qualidade</string>
<string name="search_hint">Procurar</string>
<string name="subscribe">Inscrever-se</string>
<string name="unsubscribe">CANCELAR INSCRIÇÃO</string>
<string name="share">Compartilhar</string>
<string name="subscribe">Subscrever</string>
<string name="unsubscribe">Cancelar subscrição</string>
<string name="share">Partilhar</string>
<string name="yes">Sim</string>
<string name="username">Nome de usuário</string>
<string name="login">Login</string>
<string name="register">Registrar</string>
<string name="username">Nome de utilizador</string>
<string name="login">Iniciar sessão</string>
<string name="register">Registar</string>
<string name="logout">Sair</string>
<string name="cancel">Cancelar</string>
<string name="already_logged_in">Você já está logado, você pode sair da sua conta.</string>
<string name="login_first">Por favor, faça o login e tente novamente!</string>
<string name="already_logged_in">Sessão já iniciada. Pode sair da sua conta.</string>
<string name="login_first">Inicie sessão e tente novamente.</string>
<string name="instances">Escolha uma instância</string>
<string name="customInstance">Adicionar uma instância personalizada</string>
<string name="region">Escolha uma região</string>
<string name="importsuccess">Inscrito com sucesso!</string>
<string name="subscribeIsEmpty">Inscreva-se em alguns canais primeiro!</string>
<string name="cannotDownload">Não é possível baixar esta transmissão!</string>
<string name="dlcomplete">O download foi concluído!</string>
<string name="downloadfailed">Falha no download.</string>
<string name="app_theme">Tema do aplicativo</string>
<string name="server_error">O servidor encontrou um problema. Talvez tente outra instância\?</string>
<string name="unknown_error">Erro de rede!</string>
<string name="error">Algo deu errado!</string>
<string name="notgmail">Esta não é sua conta do Gmail!</string>
<string name="customInstance">Instância personalizada</string>
<string name="region">Região</string>
<string name="importsuccess">Subscrito</string>
<string name="subscribeIsEmpty">Tem que subscrever um canal.</string>
<string name="cannotDownload">Não foi possível descarregar a emissão.</string>
<string name="dlcomplete">Descarga terminada.</string>
<string name="downloadfailed">Falha ao descarregar.</string>
<string name="app_theme">Tema</string>
<string name="server_error">Existe um problema com o servidor. Experimentar outra instância\?</string>
<string name="unknown_error">Erro de rede.</string>
<string name="error">Ocorreu um erro.</string>
<string name="notgmail">isto é apenas para uma conta Piped.</string>
<string name="defres">Resolução de vídeo padrão</string>
<string name="grid">Número de colunas da grade</string>
<string name="emptyList">Não há nada aqui!</string>
<string name="deletePlaylist">Excluir playlist</string>
<string name="areYouSure">Tem certeza de que deseja excluir esta playlist\?</string>
<string name="createPlaylist">Criar playlist</string>
<string name="playlistCreated">Playlist criada!</string>
<string name="playlistName">Nome da playlist</string>
<string name="addToPlaylist">Adicionar a playlist</string>
<string name="success">Sucesso!</string>
<string name="download">Download</string>
<string name="save">Salvar</string>
<string name="loggedIn">Login feito com sucesso!</string>
<string name="password">Senha</string>
<string name="registered">Registrado com sucesso! Agora você pode se inscrever nos canais que desejar.</string>
<string name="login_register">Entrar/Registrar</string>
<string name="please_login">Por favor, faça login ou registre-se nas configurações primeiro!</string>
<string name="dlisinprogress">Outro download já está em andamento, por favor aguarde até que seja concluído!</string>
<string name="grid">Colunas da grelha</string>
<string name="emptyList">Nada para ver aqui.</string>
<string name="deletePlaylist">Eliminar lista de reprodução</string>
<string name="areYouSure">Tem certeza de que deseja eliminar esta lista\?</string>
<string name="createPlaylist">Criar lista de reprodução</string>
<string name="playlistCreated">Lista de reprodução criada.</string>
<string name="playlistName">Nome da lista de reprodução</string>
<string name="addToPlaylist">Adicionar à lista de reprodução</string>
<string name="success">Feito.</string>
<string name="download">Descarregar</string>
<string name="save">Guardar</string>
<string name="loggedIn">Sessão iniciada.</string>
<string name="password">Palavra-passe</string>
<string name="registered">Registo efetuado. Agora já pode subscrever canais.</string>
<string name="login_register">Entrar/Registar</string>
<string name="please_login">deve iniciar sessão ou registar-se nas definições.</string>
<string name="dlisinprogress">Já existe uma descarga em curso. Por favor aguarde.</string>
<string name="vlc">Abrir no VLC</string>
<string name="vlcerror">Não é possível abrir no VLC. Talvez ainda não esteja instalado.</string>
<string name="import_from_yt">Importar inscrições do YouTube</string>
<string name="empty">Nome de usuário e senha não podem estar vazios!</string>
<string name="fail">Falhou</string>
<string name="about">Sobre</string>
<string name="vlcerror">Não foi possível abrir no VLC. Talvez não esteja instalado.</string>
<string name="import_from_yt">Importar inscrições</string>
<string name="empty">Não indicou o nome de utilizador ou a palavra-passe.</string>
<string name="fail">Falha :(</string>
<string name="about">Acerca</string>
<string name="videos">Vídeos</string>
<string name="library">Biblioteca</string>
<string name="startpage">Início</string>
<string name="subscriptions">Subscrições</string>
<string name="instance">Instância</string>
<string name="videoCount">%1$s vídeos</string>
<string name="comments">Comentários</string>
<string name="retry">Tentar novamente</string>
<string name="customization">Ajustes</string>
<string name="noInternet">Tem que estar ligado à Internet.</string>
<string name="import_from_yt_summary">De YouTube ou NewPipe</string>
<string name="changeLanguage">Idioma</string>
<string name="emptyPlaylistName">Uma lista de reprodução não pode estar vazia</string>
<string name="systemLanguage">Sistema</string>
<string name="systemDefault">Sistema</string>
<string name="lightTheme">Claro</string>
<string name="darkTheme">Escuro</string>
<string name="subscribers">%1$s subscritores</string>
<string name="settings">Definições</string>
<string name="location">Localização</string>
<string name="website">Site</string>
</resources>

View File

@ -28,7 +28,7 @@
<string name="subscribeIsEmpty">Сначала подпишитесь на некоторые каналы.</string>
<string name="cannotDownload">Невозможно скачать этот поток.</string>
<string name="dlcomplete">Загрузка завершена.</string>
<string name="dlisinprogress">Пожалуйста, подождите, пока все загрузки завершаться.</string>
<string name="dlisinprogress">Пожалуйста, подождите, пока все загрузки завершатся.</string>
<string name="downloadfailed">Загрузка не удалась.</string>
<string name="vlc">Открыть в VLC</string>
<string name="vlcerror">Не удаётся открыть в VLC. Возможно, он не установлен.</string>
@ -64,7 +64,7 @@
<string name="import_from_yt_summary">Из YouTube или NewPipe</string>
<string name="emptyPlaylistName">Название плейлиста не может быть пустым</string>
<string name="comments">Комментарии</string>
<string name="noInternet">Отсутствует соединение с сетью</string>
<string name="noInternet">Сначала подключитесь к Интернету.</string>
<string name="videoCount">%1$s видео</string>
<string name="retry">Попробовать снова</string>
<string name="settings">Настройки</string>
@ -227,4 +227,6 @@
<string name="matrix">Matrix</string>
<string name="telegram">Telegram</string>
<string name="twitter">Twitter</string>
<string name="turnInternetOn">Пожалуйста, подключитесь к Интернету, включив WiFi или мобильный интернет.</string>
<string name="open">Открыть …</string>
</resources>

View File

@ -229,4 +229,6 @@
<string name="discord">Discord</string>
<string name="open">ık …</string>
<string name="turnInternetOn">Lütfen, WiFi veya mobil verileri açarak internete bağlanın.</string>
<string name="chapters">Bölümler</string>
<string name="change_playback_speed">Oynatma hızı</string>
</resources>

View File

@ -229,4 +229,6 @@
<string name="telegram">Telegram</string>
<string name="turnInternetOn">请打开 WiFi 或移动数据连接到互联网。</string>
<string name="open">打开…</string>
<string name="chapters">章节</string>
<string name="change_playback_speed">播放速度</string>
</resources>

View File

@ -709,4 +709,16 @@
<item>portrait</item>
</string-array>
<string-array name="labelVisibility">
<item>@string/always</item>
<item>@string/selected</item>
<item>@string/never</item>
</string-array>
<string-array name="labelVisibilityValues">
<item>always</item>
<item>selected</item>
<item>never</item>
</string-array>
</resources>

View File

@ -7,9 +7,9 @@
<color name="red_light_accentDark">#EF5350</color>
<color name="red_light_background">#EDE3E4</color>
<color name="red_dark_accentLight">#B71C1C</color>
<color name="red_dark_accentDark">#B71C1C</color>
<color name="red_dark_background">#130808</color>
<color name="red_dark_accentLight">#C96052</color>
<color name="red_dark_accentDark">#D25545</color>
<color name="red_dark_background">#1B0B09</color>
<color name="blue_light_accentLight">#1F75FE</color>
<color name="blue_light_accentDark">#66A1FE</color>
@ -17,15 +17,15 @@
<color name="blue_dark_accentLight">#1F75FE</color>
<color name="blue_dark_accentDark">#0146B6</color>
<color name="blue_dark_background">#131426</color>
<color name="blue_dark_background">#0A0B15</color>
<color name="yellow_light_accentLight">#FFA000</color>
<color name="yellow_light_accentDark">#FFB300</color>
<color name="yellow_light_background">#FFF8E1</color>
<color name="yellow_dark_accentLight">#FFCA28</color>
<color name="yellow_dark_accentDark">#FF8F00</color>
<color name="yellow_dark_background">#0E0D04</color>
<color name="yellow_dark_accentLight">#D9B95C</color>
<color name="yellow_dark_accentDark">#D1B956</color>
<color name="yellow_dark_background">#19160B</color>
<color name="green_light_accentLight">#7CB342</color>
<color name="green_light_accentDark">#8BC34A</color>
@ -39,8 +39,8 @@
<color name="purple_light_accentDark">#B39DDB</color>
<color name="purple_light_background">#EFEBF6</color>
<color name="purple_dark_accentLight">#7E57C2</color>
<color name="purple_dark_accentDark">#311B92</color>
<color name="purple_dark_background">#1D0B20</color>
<color name="purple_dark_accentLight">#AA6F6B</color>
<color name="purple_dark_accentDark">#8F415B</color>
<color name="purple_dark_background">#201015</color>
</resources>

View File

@ -229,4 +229,14 @@
<string name="twitter">Twitter</string>
<string name="turnInternetOn">Please connect to the internet by turning on WiFi or mobile data.</string>
<string name="open">Open …</string>
<string name="chapters">Chapters</string>
<string name="change_playback_speed">Playback speed</string>
<string name="require_restart">Restart required</string>
<string name="require_restart_message">This change requires an app restart. Do you want to restart the app now? Otherwise the changes will be applied on the next app restart.</string>
<string name="navLabelVisibility">Navbar label visibility</string>
<string name="always">Always</string>
<string name="selected">Selected</string>
<string name="never">Never</string>
<string name="autoRotatePlayer">Auto fullscreen</string>
<string name="autoRotatePlayer_summary">Automatically switch to player fullscreen when the device gets turned.</string>
</resources>

View File

@ -15,16 +15,6 @@
</style>
<style name="OLED">
<item name="android:colorBackground">@android:color/black</item>
<item name="colorSurface">@android:color/black</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="MaterialAlertDialog">
<item name="alertDialogTheme">@style/ThemeOverlay.Material3.MaterialAlertDialog</item>
@ -114,4 +104,14 @@
</style>
<style name="PlayerButton">
<item name="android:padding">9dp</item>
<item name="android:layout_height">36dp</item>
<item name="android:layout_width">36dp</item>
<item name="android:layout_gravity">center</item>
<item name="android:background">?attr/selectableItemBackground</item>
</style>
</resources>

View File

@ -9,7 +9,7 @@
app:defaultValue="A"
app:entries="@array/themes"
app:entryValues="@array/themesValue"
app:key="theme_togglee"
app:key="theme_toggle"
app:title="@string/app_theme"
app:useSimpleSummaryProvider="true" />
@ -51,6 +51,15 @@
app:title="@string/hideTrendingPage"
app:useSimpleSummaryProvider="true" />
<ListPreference
android:icon="@drawable/ic_label"
app:defaultValue="always"
app:entries="@array/labelVisibility"
app:entryValues="@array/labelVisibilityValues"
app:key="label_visibility"
app:title="@string/navLabelVisibility"
app:useSimpleSummaryProvider="true" />
<ListPreference
android:icon="@drawable/ic_grid"
app:defaultValue="@integer/grid_items"

View File

@ -14,7 +14,7 @@
app:useSimpleSummaryProvider="true" />
<ListPreference
android:icon="@drawable/ic_player"
android:icon="@drawable/ic_videocam"
app:defaultValue="WEBM"
app:entries="@array/playerVideoFormats"
app:entryValues="@array/playerVideoFormats"
@ -60,17 +60,26 @@
<SwitchPreferenceCompat
android:icon="@drawable/ic_play_filled"
android:summary="@string/autoplay_summary"
app:defaultValue="true"
app:key="autoplay"
app:title="@string/player_autoplay" />
<SwitchPreferenceCompat
android:icon="@drawable/ic_pause_filled"
android:summary="@string/pauseOnScreenOff_summary"
app:defaultValue="false"
app:key="pause_screen_off"
app:title="@string/pauseOnScreenOff" />
<SwitchPreferenceCompat
android:icon="@drawable/ic_rotating_circle"
android:summary="@string/autoRotatePlayer_summary"
app:defaultValue="false"
app:key="auto_fullscreen"
app:title="@string/autoRotatePlayer" />
<ListPreference
android:icon="@drawable/ic_fullscreen"
android:icon="@drawable/ic_flip"
app:defaultValue="ratio"
app:entries="@array/fullscreenOrientation"
app:entryValues="@array/fullscreenOrientationValues"

View File

@ -45,7 +45,7 @@
app:title="@string/sponsorblock" />
<Preference
android:icon="@drawable/ic_player"
android:icon="@drawable/ic_movie"
app:key="player"
app:summary="@string/player_summary"
app:title="@string/audio_video" />