diff --git a/ROADMAP.md b/ROADMAP.md index 14ca29f20..8ec79d1d8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dbc40491c..8ed717cba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,7 +37,7 @@ android:exported="true" android:hardwareAccelerated="true" android:launchMode="singleTop" - android:screenOrientation="userPortrait" + android:screenOrientation="user" android:supportsPictureInPicture="true"> diff --git a/app/src/main/java/com/github/libretube/Constants.kt b/app/src/main/java/com/github/libretube/Constants.kt index 26d940916..7ef5409e0 100644 --- a/app/src/main/java/com/github/libretube/Constants.kt +++ b/app/src/main/java/com/github/libretube/Constants.kt @@ -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/" diff --git a/app/src/main/java/com/github/libretube/Globals.kt b/app/src/main/java/com/github/libretube/Globals.kt new file mode 100644 index 000000000..7d4ff4ddd --- /dev/null +++ b/app/src/main/java/com/github/libretube/Globals.kt @@ -0,0 +1,7 @@ +package com.github.libretube + +object Globals { + var isFullScreen = false + var isMiniPlayerVisible = false + var isCurrentViewMainSettings = true +} diff --git a/app/src/main/java/com/github/libretube/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/activities/MainActivity.kt index d5618ef91..dc79c69b5 100644 --- a/app/src/main/java/com/github/libretube/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/activities/MainActivity.kt @@ -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(R.id.main_container).isClickable = false val motionLayout = findViewById(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(R.id.linLayout).visibility = View.VISIBLE - isFullScreen = false + Globals.isFullScreen = false } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/app/src/main/java/com/github/libretube/activities/NoInternetActivity.kt b/app/src/main/java/com/github/libretube/activities/NoInternetActivity.kt index 0478e10b1..53f08a650 100644 --- a/app/src/main/java/com/github/libretube/activities/NoInternetActivity.kt +++ b/app/src/main/java/com/github/libretube/activities/NoInternetActivity.kt @@ -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) diff --git a/app/src/main/java/com/github/libretube/activities/SettingsActivity.kt b/app/src/main/java/com/github/libretube/activities/SettingsActivity.kt index da25444cd..5adea738a 100644 --- a/app/src/main/java/com/github/libretube/activities/SettingsActivity.kt +++ b/app/src/main/java/com/github/libretube/activities/SettingsActivity.kt @@ -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()) diff --git a/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt b/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt index 22b055a1e..6a28be080 100644 --- a/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt @@ -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) } } diff --git a/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt index 55748f397..29a14e39d 100644 --- a/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt @@ -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, @@ -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 + } } } diff --git a/app/src/main/java/com/github/libretube/adapters/SubscriptionChannelAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SubscriptionChannelAdapter.kt index 9a10ba532..6def52cb1 100644 --- a/app/src/main/java/com/github/libretube/adapters/SubscriptionChannelAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/SubscriptionChannelAdapter.kt @@ -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) : RecyclerView.Adapter() { @@ -68,17 +66,14 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList= 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() - var vidUrl = arrayListOf() + val vidName = arrayListOf() + val vidUrl = arrayListOf() // 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() - var audioUrl = arrayListOf() + val audioName = arrayListOf() + val audioUrl = arrayListOf() // 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] diff --git a/app/src/main/java/com/github/libretube/dialogs/LoginDialog.kt b/app/src/main/java/com/github/libretube/dialogs/LoginDialog.kt index 75983feb6..de8a2188c 100644 --- a/app/src/main/java/com/github/libretube/dialogs/LoginDialog.kt +++ b/app/src/main/java/com/github/libretube/dialogs/LoginDialog.kt @@ -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() } } } diff --git a/app/src/main/java/com/github/libretube/dialogs/LogoutDialog.kt b/app/src/main/java/com/github/libretube/dialogs/LogoutDialog.kt index a7192efd4..77520bd33 100644 --- a/app/src/main/java/com/github/libretube/dialogs/LogoutDialog.kt +++ b/app/src/main/java/com/github/libretube/dialogs/LogoutDialog.kt @@ -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() diff --git a/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt b/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt new file mode 100644 index 000000000..1030fe05b --- /dev/null +++ b/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt @@ -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") + } +} diff --git a/app/src/main/java/com/github/libretube/dialogs/ShareDialog.kt b/app/src/main/java/com/github/libretube/dialogs/ShareDialog.kt index a783b8e46..ed94c30ef 100644 --- a/app/src/main/java/com/github/libretube/dialogs/ShareDialog.kt +++ b/app/src/main/java/com/github/libretube/dialogs/ShareDialog.kt @@ -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 diff --git a/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt index 43aa0f0cf..ffcf56b7c 100644 --- a/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt @@ -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 } diff --git a/app/src/main/java/com/github/libretube/fragments/LibraryFragment.kt b/app/src/main/java/com/github/libretube/fragments/LibraryFragment.kt index 8ad047aa4..b5ceea33b 100644 --- a/app/src/main/java/com/github/libretube/fragments/LibraryFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/LibraryFragment.kt @@ -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() } diff --git a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt index 78e86f406..6b18f8ec3 100644 --- a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt @@ -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 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) { + 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() + 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, 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() + } + } + } } diff --git a/app/src/main/java/com/github/libretube/obj/ChapterSegment.kt b/app/src/main/java/com/github/libretube/obj/ChapterSegment.kt index 517b6d342..eb47218b3 100644 --- a/app/src/main/java/com/github/libretube/obj/ChapterSegment.kt +++ b/app/src/main/java/com/github/libretube/obj/ChapterSegment.kt @@ -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) } diff --git a/app/src/main/java/com/github/libretube/obj/DownloadType.kt b/app/src/main/java/com/github/libretube/obj/DownloadType.kt new file mode 100644 index 000000000..a94663cc7 --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/DownloadType.kt @@ -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 +} diff --git a/app/src/main/java/com/github/libretube/preferences/AdvancedSettings.kt b/app/src/main/java/com/github/libretube/preferences/AdvancedSettings.kt index f80dbafd9..750fe5f9a 100644 --- a/app/src/main/java/com/github/libretube/preferences/AdvancedSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/AdvancedSettings.kt @@ -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) diff --git a/app/src/main/java/com/github/libretube/preferences/AppearanceSettings.kt b/app/src/main/java/com/github/libretube/preferences/AppearanceSettings.kt index dbec1e361..dfe9003dc 100644 --- a/app/src/main/java/com/github/libretube/preferences/AppearanceSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/AppearanceSettings.kt @@ -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("theme_togglee") + val themeToggle = findPreference("theme_toggle") themeToggle?.setOnPreferenceChangeListener { _, _ -> - requireMainActivityRestart = true - activity?.recreate() + val restartDialog = RequireRestartDialog() + restartDialog.show(childFragmentManager, "RequireRestartDialog") true } val accentColor = findPreference("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("grid") gridColumns?.setOnPreferenceChangeListener { _, _ -> - requireMainActivityRestart = true + val restartDialog = RequireRestartDialog() + restartDialog.show(childFragmentManager, "RequireRestartDialog") true } val hideTrending = findPreference("hide_trending_page") hideTrending?.setOnPreferenceChangeListener { _, _ -> - requireMainActivityRestart = true + val restartDialog = RequireRestartDialog() + restartDialog.show(childFragmentManager, "RequireRestartDialog") + true + } + + val labelVisibilityMode = findPreference("label_visibility") + labelVisibilityMode?.setOnPreferenceChangeListener { _, _ -> + val restartDialog = RequireRestartDialog() + restartDialog.show(childFragmentManager, "RequireRestartDialog") true } } diff --git a/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt b/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt index fc902b685..169cf7358 100644 --- a/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt @@ -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("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 } diff --git a/app/src/main/java/com/github/libretube/preferences/MainSettings.kt b/app/src/main/java/com/github/libretube/preferences/MainSettings.kt index 52ee90b8f..f131e0ecd 100644 --- a/app/src/main/java/com/github/libretube/preferences/MainSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/MainSettings.kt @@ -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("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() diff --git a/app/src/main/java/com/github/libretube/preferences/PlayerSettings.kt b/app/src/main/java/com/github/libretube/preferences/PlayerSettings.kt index f2fdc3fca..b3428f4f7 100644 --- a/app/src/main/java/com/github/libretube/preferences/PlayerSettings.kt +++ b/app/src/main/java/com/github/libretube/preferences/PlayerSettings.kt @@ -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("fullscreen_orientation") + val autoRotateToFullscreen = findPreference("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 + } } } diff --git a/app/src/main/java/com/github/libretube/services/DownloadService.kt b/app/src/main/java/com/github/libretube/services/DownloadService.kt index e422a0021..63e1ffe70 100644 --- a/app/src/main/java/com/github/libretube/services/DownloadService.kt +++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt @@ -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), diff --git a/app/src/main/java/com/github/libretube/util/ConnectionHelper.kt b/app/src/main/java/com/github/libretube/util/ConnectionHelper.kt index 91020ed45..1ba230baa 100644 --- a/app/src/main/java/com/github/libretube/util/ConnectionHelper.kt +++ b/app/src/main/java/com/github/libretube/util/ConnectionHelper.kt @@ -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 } } diff --git a/app/src/main/java/com/github/libretube/util/ThemeHelper.kt b/app/src/main/java/com/github/libretube/util/ThemeHelper.kt index 0ab0a76eb..8fa3bbb68 100644 --- a/app/src/main/java/com/github/libretube/util/ThemeHelper.kt +++ b/app/src/main/java/com/github/libretube/util/ThemeHelper.kt @@ -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) { diff --git a/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt b/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt index f4de71793..808ef2320 100644 --- a/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt +++ b/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt @@ -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 } diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 000000000..fdda75ce4 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_auth.xml b/app/src/main/res/drawable/ic_auth.xml index 5d49c9e1d..0d39a3ad7 100644 --- a/app/src/main/res/drawable/ic_auth.xml +++ b/app/src/main/res/drawable/ic_auth.xml @@ -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"> - + 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" /> diff --git a/app/src/main/res/drawable/ic_flip.xml b/app/src/main/res/drawable/ic_flip.xml new file mode 100644 index 000000000..11ba464cd --- /dev/null +++ b/app/src/main/res/drawable/ic_flip.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_headphones.xml b/app/src/main/res/drawable/ic_headphones.xml new file mode 100644 index 000000000..b20cb055d --- /dev/null +++ b/app/src/main/res/drawable/ic_headphones.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_label.xml b/app/src/main/res/drawable/ic_label.xml new file mode 100644 index 000000000..5f0035189 --- /dev/null +++ b/app/src/main/res/drawable/ic_label.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_movie.xml b/app/src/main/res/drawable/ic_movie.xml new file mode 100644 index 000000000..8cebdce92 --- /dev/null +++ b/app/src/main/res/drawable/ic_movie.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_player.xml b/app/src/main/res/drawable/ic_player.xml deleted file mode 100644 index 07812f901..000000000 --- a/app/src/main/res/drawable/ic_player.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_repeat.xml b/app/src/main/res/drawable/ic_repeat.xml new file mode 100644 index 000000000..7b4c81168 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_rotating_circle.xml b/app/src/main/res/drawable/ic_rotating_circle.xml new file mode 100644 index 000000000..7339373fe --- /dev/null +++ b/app/src/main/res/drawable/ic_rotating_circle.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_videocam.xml b/app/src/main/res/drawable/ic_videocam.xml index f3699e8ab..7b2e0d03b 100644 --- a/app/src/main/res/drawable/ic_videocam.xml +++ b/app/src/main/res/drawable/ic_videocam.xml @@ -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"> - + 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" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index aed494ea4..7f6bccc71 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -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"> - + android:background="?android:attr/selectableItemBackground" + android:orientation="horizontal"> + android:layout_gravity="center" + android:layout_marginEnd="10dp" + android:orientation="vertical"> @@ -37,6 +36,15 @@ android:id="@+id/search_views" android:layout_width="wrap_content" android:layout_height="wrap_content" /> + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/channel_subscription_row.xml b/app/src/main/res/layout/channel_subscription_row.xml index 0179bb46c..591facca0 100644 --- a/app/src/main/res/layout/channel_subscription_row.xml +++ b/app/src/main/res/layout/channel_subscription_row.xml @@ -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" /> \ No newline at end of file diff --git a/app/src/main/res/layout/comments_row.xml b/app/src/main/res/layout/comments_row.xml index 73cdcc092..5256e49d6 100644 --- a/app/src/main/res/layout/comments_row.xml +++ b/app/src/main/res/layout/comments_row.xml @@ -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 @@ @@ -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" /> @@ -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" /> diff --git a/app/src/main/res/layout/exo_styled_player_control_view.xml b/app/src/main/res/layout/exo_styled_player_control_view.xml index d50a3cebf..6d56caafe 100644 --- a/app/src/main/res/layout/exo_styled_player_control_view.xml +++ b/app/src/main/res/layout/exo_styled_player_control_view.xml @@ -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" /> - + 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" /> - + - - - - + android:orientation="vertical"> - + - + - + - + - + - + - + - + - + + + + + + + + + + + @@ -187,18 +214,6 @@ - - - - + android:textSize="12sp" + app:cornerRadius="20dp" /> diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml index a44f78fb6..4ee4a9401 100644 --- a/app/src/main/res/layout/fragment_player.xml +++ b/app/src/main/res/layout/fragment_player.xml @@ -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 @@ @@ -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" /> + + + + + + + @@ -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"> - + - + + android:layout_weight=".35"> - + - + - + + android:layout_weight=".30" /> - + + + + + + + + diff --git a/app/src/main/res/layout/replies_row.xml b/app/src/main/res/layout/replies_row.xml index 1e14c6c92..6c46ed634 100644 --- a/app/src/main/res/layout/replies_row.xml +++ b/app/src/main/res/layout/replies_row.xml @@ -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" /> diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index f6997ee88..4d4d7026c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -227,4 +227,6 @@ تيليجرام ريديت تويتر + يرجى الاتصال بالإنترنت عن طريق تشغيل WiFi أو بيانات الجوال. + فتح … \ No newline at end of file diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index ef1cf069d..e2091a974 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -229,4 +229,5 @@ Discord Açıq… Zəhmət olmasa, WiFi və ya mobil datanı yandırmaqla internetə qoşulun. + Bölmələr \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a1fc41a83..6d8d7bc51 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -227,4 +227,7 @@ Comunidad Discord Twitter + Por favor, conéctese a Internet activando el WiFi o los datos móviles. + Abrir… + Capítulos \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 408aa50af..8daded198 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -227,4 +227,7 @@ Telegram Communauté Discord + Ouvrir … + Veuillez vous connecter à l\'internet en activant le WiFi ou les données mobiles. + Chapitres \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8e8abc3c8..efb702cc2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -227,4 +227,6 @@ Matrix Telegram Reddit + Connettiti a Internet attivando Wi-Fi o dati mobili. + Apri … \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 699505d4c..7e2ba3ec7 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -227,4 +227,8 @@ לאורך יחס תצוגת וידאו טוויטר + נא להתחבר לאינטרנט על ידי הפעלת הרשת האלחוטית או הנתונים הסלולריים. + פתיחה… + פרקים + מהירות נגינה \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 65983e430..2efaffbdd 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -8,6 +8,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 7c19aa60b..32b1bb944 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,58 +1,75 @@ - Desconectado com sucesso! + Sessão terminada. Qualidade Procurar - Inscrever-se - CANCELAR INSCRIÇÃO - Compartilhar + Subscrever + Cancelar subscrição + Partilhar Sim - Nome de usuário - Login - Registrar + Nome de utilizador + Iniciar sessão + Registar Sair Cancelar - Você já está logado, você pode sair da sua conta. - Por favor, faça o login e tente novamente! + Sessão já iniciada. Pode sair da sua conta. + Inicie sessão e tente novamente. Escolha uma instância - Adicionar uma instância personalizada - Escolha uma região - Inscrito com sucesso! - Inscreva-se em alguns canais primeiro! - Não é possível baixar esta transmissão! - O download foi concluído! - Falha no download. - Tema do aplicativo - O servidor encontrou um problema. Talvez tente outra instância\? - Erro de rede! - Algo deu errado! - Esta não é sua conta do Gmail! + Instância personalizada + Região + Subscrito + Tem que subscrever um canal. + Não foi possível descarregar a emissão. + Descarga terminada. + Falha ao descarregar. + Tema + Existe um problema com o servidor. Experimentar outra instância\? + Erro de rede. + Ocorreu um erro. + isto é apenas para uma conta Piped. Resolução de vídeo padrão - Número de colunas da grade - Não há nada aqui! - Excluir playlist - Tem certeza de que deseja excluir esta playlist\? - Criar playlist - Playlist criada! - Nome da playlist - Adicionar a playlist - Sucesso! - Download - Salvar - Login feito com sucesso! - Senha - Registrado com sucesso! Agora você pode se inscrever nos canais que desejar. - Entrar/Registrar - Por favor, faça login ou registre-se nas configurações primeiro! - Outro download já está em andamento, por favor aguarde até que seja concluído! + Colunas da grelha + Nada para ver aqui. + Eliminar lista de reprodução + Tem certeza de que deseja eliminar esta lista\? + Criar lista de reprodução + Lista de reprodução criada. + Nome da lista de reprodução + Adicionar à lista de reprodução + Feito. + Descarregar + Guardar + Sessão iniciada. + Palavra-passe + Registo efetuado. Agora já pode subscrever canais. + Entrar/Registar + deve iniciar sessão ou registar-se nas definições. + Já existe uma descarga em curso. Por favor aguarde. Abrir no VLC - Não é possível abrir no VLC. Talvez ainda não esteja instalado. - Importar inscrições do YouTube - Nome de usuário e senha não podem estar vazios! - Falhou - Sobre + Não foi possível abrir no VLC. Talvez não esteja instalado. + Importar inscrições + Não indicou o nome de utilizador ou a palavra-passe. + Falha :( + Acerca Vídeos Biblioteca Início Subscrições + Instância + %1$s vídeos + Comentários + Tentar novamente + Ajustes + Tem que estar ligado à Internet. + De YouTube ou NewPipe + Idioma + Uma lista de reprodução não pode estar vazia + Sistema + Sistema + Claro + Escuro + %1$s subscritores + Definições + Localização + Site \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f53435fa0..c10cf251e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -28,7 +28,7 @@ Сначала подпишитесь на некоторые каналы. Невозможно скачать этот поток. Загрузка завершена. - Пожалуйста, подождите, пока все загрузки завершаться. + Пожалуйста, подождите, пока все загрузки завершатся. Загрузка не удалась. Открыть в VLC Не удаётся открыть в VLC. Возможно, он не установлен. @@ -64,7 +64,7 @@ Из YouTube или NewPipe Название плейлиста не может быть пустым Комментарии - Отсутствует соединение с сетью + Сначала подключитесь к Интернету. %1$s видео Попробовать снова Настройки @@ -227,4 +227,6 @@ Matrix Telegram Twitter + Пожалуйста, подключитесь к Интернету, включив WiFi или мобильный интернет. + Открыть … \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ac71f513e..82faa7bdb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -229,4 +229,6 @@ Discord Açık … Lütfen, WiFi veya mobil verileri açarak internete bağlanın. + Bölümler + Oynatma hızı \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cbcfff298..5ced3fd85 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -229,4 +229,6 @@ Telegram 请打开 WiFi 或移动数据连接到互联网。 打开… + 章节 + 播放速度 \ No newline at end of file diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index f57787c26..6c7eac38c 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -709,4 +709,16 @@ portrait + + @string/always + @string/selected + @string/never + + + + always + selected + never + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a36b45514..ecb63c05c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,9 +7,9 @@ #EF5350 #EDE3E4 - #B71C1C - #B71C1C - #130808 + #C96052 + #D25545 + #1B0B09 #1F75FE #66A1FE @@ -17,15 +17,15 @@ #1F75FE #0146B6 - #131426 + #0A0B15 #FFA000 #FFB300 #FFF8E1 - #FFCA28 - #FF8F00 - #0E0D04 + #D9B95C + #D1B956 + #19160B #7CB342 #8BC34A @@ -39,8 +39,8 @@ #B39DDB #EFEBF6 - #7E57C2 - #311B92 - #1D0B20 + #AA6F6B + #8F415B + #201015 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b44d5347..ac64401d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,4 +229,14 @@ Twitter Please connect to the internet by turning on WiFi or mobile data. Open … + Chapters + Playback speed + Restart required + 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. + Navbar label visibility + Always + Selected + Never + Auto fullscreen + Automatically switch to player fullscreen when the device gets turned. \ No newline at end of file diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml index 3db13793b..b40cc63da 100644 --- a/app/src/main/res/values/style.xml +++ b/app/src/main/res/values/style.xml @@ -15,16 +15,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/xml/appearance_settings.xml b/app/src/main/res/xml/appearance_settings.xml index f044c2967..35b1aae90 100644 --- a/app/src/main/res/xml/appearance_settings.xml +++ b/app/src/main/res/xml/appearance_settings.xml @@ -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" /> + + + +