diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index b103f9b56..4bb39afdb 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,2 @@
patreon: LibreTubeTeam
-custom: https://libre-tube.github.io#donate
+custom: https://github.com/libre-tube/LibreTube#-donate
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..3ba13e0ce
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/README.md b/README.md
index 9cb63c092..4a2941890 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,29 @@
-[
](https://f-droid.org/en/packages/com.github.libretube/)
-[
](https://apt.izzysoft.de/fdroid/index/apk/com.github.libretube)
-[
](https://github.com/libre-tube/LibreTube/releases/latest)
-[
](https://t.me/LibreTube)
+[
](https://f-droid.org/en/packages/com.github.libretube/)
+[
](https://apt.izzysoft.de/fdroid/index/apk/com.github.libretube)
+[
](https://github.com/libre-tube/LibreTube/releases/latest)
+[
](https://t.me/LibreTube)
+## 📔 About
+
+YouTube has an extremely invasive privacy policy which relies on using user data in unethical ways. They store a lot of your personal data - ranging from ideas, music taste, content, political opinions, and much more than you think.
+
+The project is aimed to improve the users privacy by being independent from Google and bypassing their data collection. Therefore the app is using the [Piped API](https://github.com/TeamPiped/Piped), which uses proxies to circumvent Googles data collection and includes some other additional features.
+
+> **⚠️ WARNING: This is a beta version, therefore you may encounter bugs. If you do, open an issue via our GitHub repository.**
+
## 📱 Screenshots
@@ -47,14 +55,12 @@
## 😇 Contributing
-Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.The more is done the better it gets!
+Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. The more is done, the better it gets!
If creating a pull request, please make sure to format your code (preferred ktlint) before.
If opening an issue without following the issue template, we will ignore the issue and force close it.
-> **⚠️ WARNING: This is a beta version, therefore you may encounter bugs. If you do, open an issue via our github repository.**
-
### 📝 Translation
@@ -73,3 +79,6 @@ If opening an issue without following the issue template, we will ignore the iss
GitLab
NotABug
+
diff --git a/app/build.gradle b/app/build.gradle
index d0ed08e1b..f3948200e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,9 @@
+import java.time.Instant
+
plugins {
id 'com.android.application'
id 'kotlin-android'
+ id 'kotlin-android-extensions'
}
android {
@@ -10,8 +13,8 @@ android {
applicationId 'com.github.libretube'
minSdk 21
targetSdk 31
- versionCode 13
- versionName '0.3.3'
+ versionCode 16
+ versionName '0.4.2'
multiDexEnabled true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
resValue "string", "app_name", "LibreTube"
@@ -21,6 +24,16 @@ android {
viewBinding true
}
+ applicationVariants.all { variant ->
+ // use the date as version for debug builds
+ if (variant.name == 'debug') {
+ variant.outputs.each { output ->
+ output.versionCodeOverride = getUnixTime()
+ output.versionNameOverride = getUnixTime()
+ }
+ }
+ }
+
buildTypes {
release {
minifyEnabled true
@@ -68,6 +81,7 @@ dependencies {
implementation libs.androidx.navigation.fragment
implementation libs.androidx.navigation.ui
implementation libs.androidx.preference
+ implementation libs.androidx.work.runtime
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espressoCore
@@ -79,15 +93,21 @@ dependencies {
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
implementation libs.exoplayer.extension.mediasession
- implementation libs.square.picasso
implementation libs.square.retrofit
implementation libs.square.retrofit.converterJackson
// Do not update jackson annotations! It does not supports < API 26.
implementation libs.jacksonAnnotations
- implementation libs.mobileffmpeg
-
coreLibraryDesugaring libs.desugaring
implementation libs.cronet.embedded
- implementation libs.gson
+ implementation libs.cronet.okhttp
+ implementation libs.coil
+
+ implementation libs.lifecycle.viewmodel
+ implementation libs.lifecycle.runtime
+ implementation libs.lifecycle.livedata
}
+
+static def getUnixTime() {
+ return Instant.now().getEpochSecond()
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 6cd107af9..b7d85991e 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -14,7 +14,7 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
+-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
@@ -22,3 +22,9 @@
#uncomment for debug
#-keepnames class **
-keep class com.github.libretube.obj.** { *; }
+
+# prevents android from removing it
+-keep class com.github.libretube.update.** { *; }
+
+# prevents obfuscation in debug logs
+-dontobfuscate
\ No newline at end of file
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
index 9e7eaaa0c..2dee107a4 100644
--- a/app/release/output-metadata.json
+++ b/app/release/output-metadata.json
@@ -16,10 +16,23 @@
}
],
"attributes": [],
- "versionCode": 7,
- "versionName": "0.2.5",
+ "versionCode": 16,
+ "versionName": "0.4.2",
"outputFile": "app-x86_64-release.apk"
},
+ {
+ "type": "ONE_OF_MANY",
+ "filters": [
+ {
+ "filterType": "ABI",
+ "value": "arm64-v8a"
+ }
+ ],
+ "attributes": [],
+ "versionCode": 16,
+ "versionName": "0.4.2",
+ "outputFile": "app-arm64-v8a-release.apk"
+ },
{
"type": "ONE_OF_MANY",
"filters": [
@@ -29,8 +42,8 @@
}
],
"attributes": [],
- "versionCode": 7,
- "versionName": "0.2.5",
+ "versionCode": 16,
+ "versionName": "0.4.2",
"outputFile": "app-x86-release.apk"
},
{
@@ -42,22 +55,9 @@
}
],
"attributes": [],
- "versionCode": 7,
- "versionName": "0.2.5",
+ "versionCode": 16,
+ "versionName": "0.4.2",
"outputFile": "app-armeabi-v7a-release.apk"
- },
- {
- "type": "ONE_OF_MANY",
- "filters": [
- {
- "filterType": "ABI",
- "value": "arm64-v8a"
- }
- ],
- "attributes": [],
- "versionCode": 7,
- "versionName": "0.2.5",
- "outputFile": "app-arm64-v8a-release.apk"
}
],
"elementType": "File"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8ed717cba..21478eef8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -7,7 +8,9 @@
-
+
-
+ android:theme="@style/Theme.Purple.Pure"
+ tools:targetApi="n">
+
+
+
+
+
+
+
+ android:exported="false" />
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/github/libretube/Constants.kt b/app/src/main/java/com/github/libretube/Constants.kt
index 7ef5409e0..7c5b6c075 100644
--- a/app/src/main/java/com/github/libretube/Constants.kt
+++ b/app/src/main/java/com/github/libretube/Constants.kt
@@ -12,6 +12,7 @@ const val WEBSITE_URL = "https://libre-tube.github.io/"
const val DONATE_URL = "https://github.com/libre-tube/LibreTube#donate"
const val GITHUB_URL = "https://github.com/libre-tube/LibreTube"
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped"
+const val WEBLATE_URL = "https://hosted.weblate.org/projects/libretube/libretube/"
/**
* Social media links for the community fragment
@@ -32,3 +33,19 @@ const val YOUTUBE_FRONTEND_URL = "https://www.youtube.com"
* Retrofit Instance
*/
const val PIPED_API_URL = "https://pipedapi.kavin.rocks/"
+
+/**
+ * Notification IDs
+ */
+const val PLAYER_NOTIFICATION_ID = 1
+const val PUSH_NOTIFICATION_ID = 2
+const val DOWNLOAD_PENDING_NOTIFICATION_ID = 3
+const val DOWNLOAD_FAILURE_NOTIFICATION_ID = 4
+const val DOWNLOAD_SUCCESS_NOTIFICATION_ID = 5
+
+/**
+ * Notification Channel IDs
+ */
+const val DOWNLOAD_CHANNEL_ID = "download_service"
+const val BACKGROUND_CHANNEL_ID = "background_mode"
+const val PUSH_CHANNEL_ID = "notification_worker"
diff --git a/app/src/main/java/com/github/libretube/Globals.kt b/app/src/main/java/com/github/libretube/Globals.kt
index 7d4ff4ddd..5db9eabea 100644
--- a/app/src/main/java/com/github/libretube/Globals.kt
+++ b/app/src/main/java/com/github/libretube/Globals.kt
@@ -1,7 +1,22 @@
package com.github.libretube
+/**
+ * Global variables can be stored here
+ */
object Globals {
- var isFullScreen = false
- var isMiniPlayerVisible = false
- var isCurrentViewMainSettings = true
+ // for the player fragment
+ var IS_FULL_SCREEN = false
+ var MINI_PLAYER_VISIBLE = false
+
+ // for the data saver mode
+ var DATA_SAVER_MODE_ENABLED = false
+
+ // for downloads
+ var IS_DOWNLOAD_RUNNING = false
+
+ // for playlists
+ var SELECTED_PLAYLIST_ID: String? = null
+
+ // history of played videos in the current lifecycle
+ val playingQueue = mutableListOf
()
}
diff --git a/app/src/main/java/com/github/libretube/MyApp.kt b/app/src/main/java/com/github/libretube/MyApp.kt
index 2995e496a..11cfacfd7 100644
--- a/app/src/main/java/com/github/libretube/MyApp.kt
+++ b/app/src/main/java/com/github/libretube/MyApp.kt
@@ -4,12 +4,69 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
+import android.os.StrictMode
+import android.os.StrictMode.VmPolicy
+import androidx.work.ExistingPeriodicWorkPolicy
+import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
+import com.github.libretube.util.ExceptionHandler
+import com.github.libretube.util.NotificationHelper
+import com.github.libretube.util.RetrofitInstance
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
+ /**
+ * initialize the needed [NotificationChannel]s for DownloadService and BackgroundMode
+ */
initializeNotificationChannels()
+
+ /**
+ * set the applicationContext as context for the [PreferenceHelper]
+ */
+ PreferenceHelper.setContext(applicationContext)
+
+ /**
+ * bypassing fileUriExposedException, see https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed
+ */
+ val builder = VmPolicy.Builder()
+ StrictMode.setVmPolicy(builder.build())
+
+ /**
+ * set the api and the auth api url
+ */
+ setRetrofitApiUrls()
+
+ /**
+ * initialize the notification listener in the background
+ */
+ NotificationHelper.enqueueWork(this, ExistingPeriodicWorkPolicy.KEEP)
+
+ /**
+ * Handler for uncaught exceptions
+ */
+ val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
+ val exceptionHandler = ExceptionHandler(defaultExceptionHandler)
+ Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
+ }
+
+ /**
+ * set the api urls needed for the [RetrofitInstance]
+ */
+ private fun setRetrofitApiUrls() {
+ RetrofitInstance.url =
+ PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)
+ // set auth instance
+ RetrofitInstance.authUrl =
+ if (PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
+ PreferenceHelper.getString(
+ PreferenceKeys.AUTH_INSTANCE,
+ PIPED_API_URL
+ )
+ } else {
+ RetrofitInstance.url
+ }
}
/**
@@ -17,17 +74,23 @@ class MyApp : Application() {
*/
private fun initializeNotificationChannels() {
createNotificationChannel(
- "download_service",
+ DOWNLOAD_CHANNEL_ID,
"Download Service",
- "DownloadService",
+ "Shows a notification when downloading media.",
NotificationManager.IMPORTANCE_NONE
)
createNotificationChannel(
- "background_mode",
+ BACKGROUND_CHANNEL_ID,
"Background Mode",
"Shows a notification with buttons to control the audio player",
NotificationManager.IMPORTANCE_LOW
)
+ createNotificationChannel(
+ PUSH_CHANNEL_ID,
+ "Notification Worker",
+ "Shows a notification when new streams are available.",
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
}
private fun createNotificationChannel(
@@ -46,9 +109,4 @@ class MyApp : Application() {
notificationManager.createNotificationChannel(channel)
}
}
-
- companion object {
- @JvmField
- var seekTo: Long? = 0
- }
}
diff --git a/app/src/main/java/com/github/libretube/preferences/AboutFragment.kt b/app/src/main/java/com/github/libretube/activities/AboutActivity.kt
similarity index 60%
rename from app/src/main/java/com/github/libretube/preferences/AboutFragment.kt
rename to app/src/main/java/com/github/libretube/activities/AboutActivity.kt
index 0605e6707..a6a23a862 100644
--- a/app/src/main/java/com/github/libretube/preferences/AboutFragment.kt
+++ b/app/src/main/java/com/github/libretube/activities/AboutActivity.kt
@@ -1,48 +1,35 @@
-package com.github.libretube.preferences
+package com.github.libretube.activities
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Html
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
import com.github.libretube.DONATE_URL
import com.github.libretube.GITHUB_URL
import com.github.libretube.PIPED_GITHUB_URL
import com.github.libretube.R
+import com.github.libretube.WEBLATE_URL
import com.github.libretube.WEBSITE_URL
-import com.github.libretube.activities.SettingsActivity
-import com.github.libretube.databinding.FragmentAboutBinding
-import com.github.libretube.util.ThemeHelper.getThemeColor
+import com.github.libretube.databinding.ActivityAboutBinding
+import com.github.libretube.extensions.BaseActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
-class AboutFragment : Fragment() {
- private lateinit var binding: FragmentAboutBinding
+class AboutActivity : BaseActivity() {
+ private lateinit var binding: ActivityAboutBinding
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- binding = FragmentAboutBinding.inflate(layoutInflater)
- return binding.root
- }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val settingsActivity = activity as SettingsActivity
- settingsActivity.changeTopBarText(getString(R.string.about))
+ binding = ActivityAboutBinding.inflate(layoutInflater)
+ setContentView(binding.root)
binding.website.setOnClickListener {
openLinkFromHref(WEBSITE_URL)
}
binding.website.setOnLongClickListener {
- val text = context?.getString(R.string.website_summary)!!
+ val text = getString(R.string.website_summary)
showSnackBar(text)
true
}
@@ -51,7 +38,16 @@ class AboutFragment : Fragment() {
openLinkFromHref(PIPED_GITHUB_URL)
}
binding.piped.setOnLongClickListener {
- val text = context?.getString(R.string.piped_summary)!!
+ val text = getString(R.string.piped_summary)
+ showSnackBar(text)
+ true
+ }
+
+ binding.translate.setOnClickListener {
+ openLinkFromHref(WEBLATE_URL)
+ }
+ binding.translate.setOnLongClickListener {
+ val text = getString(R.string.translate_summary)
showSnackBar(text)
true
}
@@ -60,7 +56,7 @@ class AboutFragment : Fragment() {
openLinkFromHref(DONATE_URL)
}
binding.donate.setOnLongClickListener {
- val text = context?.getString(R.string.donate_summary)!!
+ val text = getString(R.string.donate_summary)
showSnackBar(text)
true
}
@@ -69,7 +65,7 @@ class AboutFragment : Fragment() {
openLinkFromHref(GITHUB_URL)
}
binding.github.setOnLongClickListener {
- val text = context?.getString(R.string.contributing_summary)!!
+ val text = getString(R.string.contributing_summary)
showSnackBar(text)
true
}
@@ -78,7 +74,7 @@ class AboutFragment : Fragment() {
showLicense()
}
binding.license.setOnLongClickListener {
- val text = context?.getString(R.string.license_summary)!!
+ val text = getString(R.string.license_summary)
showSnackBar(text)
true
}
@@ -94,10 +90,6 @@ class AboutFragment : Fragment() {
val snackBar = Snackbar
.make(binding.root, text, Snackbar.LENGTH_LONG)
- // set snackBar color
- snackBar.setBackgroundTint(getThemeColor(requireContext(), R.attr.colorSurface))
- snackBar.setTextColor(getThemeColor(requireContext(), R.attr.colorPrimary))
-
// prevent the text from being partially hidden
snackBar.setTextMaxLines(3)
@@ -105,7 +97,6 @@ class AboutFragment : Fragment() {
}
private fun showLicense() {
- val assets = view?.context?.assets
val licenseString = assets
?.open("gpl3.html")
?.bufferedReader()
@@ -119,7 +110,7 @@ class AboutFragment : Fragment() {
Html.fromHtml(licenseString.toString())
}
- MaterialAlertDialogBuilder(requireContext())
+ MaterialAlertDialogBuilder(this)
.setPositiveButton(getString(R.string.okay)) { _, _ -> }
.setMessage(licenseHtml)
.create()
diff --git a/app/src/main/java/com/github/libretube/preferences/CommunityFragment.kt b/app/src/main/java/com/github/libretube/activities/CommunityActivity.kt
similarity index 51%
rename from app/src/main/java/com/github/libretube/preferences/CommunityFragment.kt
rename to app/src/main/java/com/github/libretube/activities/CommunityActivity.kt
index 90b2d9a5b..7fe9f2211 100644
--- a/app/src/main/java/com/github/libretube/preferences/CommunityFragment.kt
+++ b/app/src/main/java/com/github/libretube/activities/CommunityActivity.kt
@@ -1,38 +1,24 @@
-package com.github.libretube.preferences
+package com.github.libretube.activities
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
import com.github.libretube.DISCORD_URL
import com.github.libretube.MATRIX_URL
-import com.github.libretube.R
import com.github.libretube.REDDIT_URL
import com.github.libretube.TELEGRAM_URL
import com.github.libretube.TWITTER_URL
-import com.github.libretube.activities.SettingsActivity
-import com.github.libretube.databinding.FragmentCommunityBinding
+import com.github.libretube.databinding.ActivityCommunityBinding
+import com.github.libretube.extensions.BaseActivity
-class CommunityFragment : Fragment() {
- private lateinit var binding: FragmentCommunityBinding
+class CommunityActivity : BaseActivity() {
+ private lateinit var binding: ActivityCommunityBinding
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- binding = FragmentCommunityBinding.inflate(layoutInflater)
- return binding.root
- }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val settingsActivity = activity as SettingsActivity
- settingsActivity.changeTopBarText(getString(R.string.community))
+ binding = ActivityCommunityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
binding.telegram.setOnClickListener {
openLinkFromHref(TELEGRAM_URL)
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 dc79c69b5..901945509 100644
--- a/app/src/main/java/com/github/libretube/activities/MainActivity.kt
+++ b/app/src/main/java/com/github/libretube/activities/MainActivity.kt
@@ -1,8 +1,7 @@
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
@@ -10,70 +9,81 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
-import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
-import androidx.appcompat.app.AppCompatActivity
+import android.widget.Toast
+import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf
-import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
+import coil.ImageLoader
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.dialogs.ErrorDialog
+import com.github.libretube.extensions.BaseActivity
import com.github.libretube.fragments.PlayerFragment
+import com.github.libretube.models.SearchViewModel
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.services.ClosingService
import com.github.libretube.util.ConnectionHelper
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.navigation.NavigationBarView
-class MainActivity : AppCompatActivity() {
+class MainActivity : BaseActivity() {
val TAG = "MainActivity"
lateinit var binding: ActivityMainBinding
lateinit var navController: NavController
private var startFragmentId = R.id.homeFragment
+ var autoRotationEnabled = false
+
+ lateinit var searchView: SearchView
override fun onCreate(savedInstanceState: Bundle?) {
- // set the app theme (e.g. Material You)
- ThemeHelper.updateTheme(this)
-
// set the language
LocaleHelper.updateLanguage(this)
super.onCreate(savedInstanceState)
+ autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
+
+ // enable auto rotation if turned on
+ requestedOrientation = if (autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
+ else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+
// start service that gets called on closure
- startService(Intent(this, ClosingService::class.java))
+ try {
+ startService(Intent(this, ClosingService::class.java))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
CronetHelper.initCronet(this.applicationContext)
+ ConnectionHelper.imageLoader = ImageLoader.Builder(this.applicationContext)
+ .callFactory(CronetHelper.callFactory)
+ .build()
- RetrofitInstance.url =
- PreferenceHelper.getString(this, "selectInstance", PIPED_API_URL)!!
- // set auth instance
- RetrofitInstance.authUrl =
- if (PreferenceHelper.getBoolean(this, "auth_instance_toggle", false)) {
- PreferenceHelper.getString(
- this,
- "selectAuthInstance",
- PIPED_API_URL
- )!!
- } else {
- RetrofitInstance.url
- }
+ // save whether the data saver mode is enabled
+ Globals.DATA_SAVER_MODE_ENABLED = PreferenceHelper.getBoolean(
+ PreferenceKeys.DATA_SAVER_MODE,
+ false
+ )
// show noInternet Activity if no internet available on app startup
if (!ConnectionHelper.isNetworkAvailable(this)) {
@@ -83,6 +93,9 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ // set the action bar for the activity
+ setSupportActionBar(binding.toolbar)
+
navController = findNavController(R.id.fragment)
binding.bottomNav.setupWithNavController(navController)
@@ -93,17 +106,19 @@ class MainActivity : AppCompatActivity() {
window.navigationBarColor = color
// hide the trending page if enabled
- val hideTrendingPage = PreferenceHelper.getBoolean(this, "hide_trending_page", false)
+ val hideTrendingPage =
+ PreferenceHelper.getBoolean(PreferenceKeys.HIDE_TRENDING_PAGE, false)
if (hideTrendingPage) binding.bottomNav.menu.findItem(R.id.homeFragment).isVisible =
false
// save start tab fragment id
- startFragmentId = when (PreferenceHelper.getString(this, "default_tab", "home")) {
- "home" -> R.id.homeFragment
- "subscriptions" -> R.id.subscriptionsFragment
- "library" -> R.id.libraryFragment
- else -> R.id.homeFragment
- }
+ startFragmentId =
+ when (PreferenceHelper.getString(PreferenceKeys.DEFAULT_TAB, "home")) {
+ "home" -> R.id.homeFragment
+ "subscriptions" -> R.id.subscriptionsFragment
+ "library" -> R.id.libraryFragment
+ else -> R.id.homeFragment
+ }
// set default tab as start fragment
navController.graph.setStartDestination(startFragmentId)
@@ -112,7 +127,7 @@ class MainActivity : AppCompatActivity() {
navController.navigate(startFragmentId)
val labelVisibilityMode = when (
- PreferenceHelper.getString(this, "label_visibility", "always")
+ PreferenceHelper.getString(PreferenceKeys.LABEL_VISIBILITY, "always")
) {
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
@@ -121,10 +136,13 @@ class MainActivity : AppCompatActivity() {
}
binding.bottomNav.labelVisibilityMode = labelVisibilityMode
+ binding.bottomNav.setOnApplyWindowInsetsListener(null)
+
binding.bottomNav.setOnItemSelectedListener {
// clear backstack if it's the start fragment
if (startFragmentId == it.itemId) navController.backQueue.clear()
// set menu item on click listeners
+ removeSearchFocus()
when (it.itemId) {
R.id.homeFragment -> {
navController.navigate(R.id.homeFragment)
@@ -140,22 +158,140 @@ class MainActivity : AppCompatActivity() {
}
binding.toolbar.title = ThemeHelper.getStyledAppName(this)
+ }
- binding.toolbar.setNavigationOnClickListener {
- // settings activity stuff
- val intent = Intent(this, SettingsActivity::class.java)
- startActivity(intent)
- }
+ /**
+ * handle error logs
+ */
+ val log = PreferenceHelper.getErrorLog()
+ if (log != "") ErrorDialog().show(supportFragmentManager, null)
- binding.toolbar.setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_search -> {
- navController.navigate(R.id.searchFragment)
+ setupBreakReminder()
+ }
+
+ /**
+ * Show a break reminder when watched too long
+ */
+ private fun setupBreakReminder() {
+ val breakReminderPref = PreferenceHelper.getString(
+ PreferenceKeys.BREAK_REMINDER,
+ "disabled"
+ )
+ if (breakReminderPref == "disabled") return
+ Handler(Looper.getMainLooper()).postDelayed(
+ {
+ try {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.share_with_time))
+ .setMessage(
+ getString(
+ R.string.already_spent_time,
+ breakReminderPref
+ )
+ )
+ .setPositiveButton(R.string.okay, null)
+ .show()
+ } catch (e: Exception) {
+ kotlin.runCatching {
+ Toast.makeText(this, R.string.take_a_break, Toast.LENGTH_LONG).show()
}
}
- false
+ },
+ breakReminderPref.toLong() * 60 * 1000
+ )
+ }
+
+ private fun removeSearchFocus() {
+ searchView.setQuery("", false)
+ searchView.clearFocus()
+ searchView.onActionViewCollapsed()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ menuInflater.inflate(R.menu.action_bar, menu)
+
+ // stuff for the search in the topBar
+ val searchItem = menu.findItem(R.id.action_search)
+ searchView = searchItem.actionView as SearchView
+
+ val searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
+
+ searchView.setOnSearchClickListener {
+ if (navController.currentDestination?.id != R.id.searchResultFragment) {
+ searchViewModel.setQuery(null)
+ navController.navigate(R.id.searchFragment)
}
}
+
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ val bundle = Bundle()
+ bundle.putString("query", query)
+ navController.navigate(R.id.searchResultFragment, bundle)
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ if (navController.currentDestination?.id != R.id.searchFragment) {
+ val bundle = Bundle()
+ bundle.putString("query", newText)
+ navController.navigate(R.id.searchFragment, bundle)
+ } else {
+ searchViewModel.setQuery(newText)
+ }
+ return true
+ }
+ })
+
+ searchItem.setOnActionExpandListener(
+ object : MenuItem.OnActionExpandListener {
+ override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
+ val currentFragmentId = navController.currentDestination?.id
+ if (currentFragmentId == R.id.searchFragment || currentFragmentId == R.id.searchResultFragment) {
+ onBackPressed()
+ }
+ return true
+ }
+ }
+ )
+
+ searchView.setOnCloseListener {
+ if (navController.currentDestination?.id == R.id.searchFragment) {
+ searchViewModel.setQuery(null)
+ onBackPressed()
+ }
+ false
+ }
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ return when (item.itemId) {
+ R.id.action_settings -> {
+ val settingsIntent = Intent(this, SettingsActivity::class.java)
+ startActivity(settingsIntent)
+ true
+ }
+ R.id.action_about -> {
+ val aboutIntent = Intent(this, AboutActivity::class.java)
+ startActivity(aboutIntent)
+ true
+ }
+ R.id.action_community -> {
+ val communityIntent = Intent(this, CommunityActivity::class.java)
+ startActivity(communityIntent)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
}
override fun onStart() {
@@ -163,92 +299,95 @@ class MainActivity : AppCompatActivity() {
val intentData: Uri? = intent?.data
// check whether an URI got submitted over the intent data
if (intentData != null && intentData.host != null && intentData.path != null) {
- Log.d("intentData", "${intentData.host} ${intentData.path} ")
+ Log.d(TAG, "intentData: ${intentData.host} ${intentData.path} ")
// load the URI of the submitted link (e.g. video)
loadIntentData(intentData)
}
}
private fun loadIntentData(data: Uri) {
- // channel
- if (data.path!!.contains("/channel/") ||
+ if (data.path!!.contains("/channel/")
+ ) {
+ val channelId = data.path!!
+ .replace("/channel/", "")
+
+ loadChannel(channelId = channelId)
+ } else if (
data.path!!.contains("/c/") ||
data.path!!.contains("/user/")
) {
- Log.i(TAG, "URI Type: Channel")
- var channel = data.path
- channel = channel!!.replace("/c/", "")
- channel = channel.replace("/user/", "")
- val bundle = bundleOf("channel_id" to channel)
- navController.navigate(R.id.channelFragment, bundle)
- } else if (data.path!!.contains("/playlist")) {
- Log.i(TAG, "URI Type: Playlist")
- var playlist = data.query!!
- if (playlist.contains("&")) {
- val playlists = playlist.split("&")
- for (v in playlists) {
+ val channelName = data.path!!
+ .replace("/c/", "")
+ .replace("/user/", "")
+
+ loadChannel(channelName = channelName)
+ } else if (
+ data.path!!.contains("/playlist")
+ ) {
+ var playlistId = data.query!!
+ if (playlistId.contains("&")) {
+ for (v in playlistId.split("&")) {
if (v.contains("list=")) {
- playlist = v
+ playlistId = v.replace("list=", "")
break
}
}
+ } else {
+ playlistId = playlistId.replace("list=", "")
}
- playlist = playlist.replace("list=", "")
- val bundle = bundleOf("playlist_id" to playlist)
- navController.navigate(R.id.playlistFragment, bundle)
- } else if (data.path!!.contains("/shorts/") ||
+
+ loadPlaylist(playlistId)
+ } else if (
+ data.path!!.contains("/shorts/") ||
data.path!!.contains("/embed/") ||
data.path!!.contains("/v/")
) {
- Log.i(TAG, "URI Type: Video")
- val watch = data.path!!
+ val videoId = data.path!!
.replace("/shorts/", "")
.replace("/v/", "")
.replace("/embed/", "")
- val bundle = Bundle()
- bundle.putString("videoId", watch)
- // for time stamped links
- if (data.query != null && data.query?.contains("t=")!!) {
- val timeStamp = data.query.toString().split("t=")[1]
- bundle.putLong("timeStamp", timeStamp.toLong())
- }
- loadWatch(bundle)
+
+ loadVideo(videoId, data.query)
} else if (data.path!!.contains("/watch") && data.query != null) {
- Log.d("dafaq", data.query!!)
- var watch = data.query!!
- if (watch.contains("&")) {
- val watches = watch.split("&")
+ var videoId = data.query!!
+
+ if (videoId.contains("&")) {
+ val watches = videoId.split("&")
for (v in watches) {
if (v.contains("v=")) {
- watch = v
+ videoId = v.replace("v=", "")
break
}
}
+ } else {
+ videoId = videoId
+ .replace("v=", "")
}
- val bundle = Bundle()
- bundle.putString("videoId", watch.replace("v=", ""))
- // for time stamped links
- if (data.query != null && data.query?.contains("t=")!!) {
- val timeStamp = data.query.toString().split("t=")[1]
- bundle.putLong("timeStamp", timeStamp.toLong())
- }
- loadWatch(bundle)
+
+ loadVideo(videoId, data.query)
} else {
- val watch = data.path!!.replace("/", "")
- val bundle = Bundle()
- bundle.putString("videoId", watch)
- // for time stamped links
- if (data.query != null && data.query?.contains("t=")!!) {
- val timeStamp = data.query.toString().split("t=")[1]
- bundle.putLong("timeStamp", timeStamp.toLong())
- }
- loadWatch(bundle)
+ val videoId = data.path!!.replace("/", "")
+
+ loadVideo(videoId, data.query)
}
}
- private fun loadWatch(bundle: Bundle) {
+ private fun loadVideo(videoId: String, query: String?) {
+ Log.i(TAG, "URI type: Video")
+
+ val bundle = Bundle()
+ Log.e(TAG, videoId)
+
+ // for time stamped links
+ if (query != null && query.contains("t=")) {
+ val timeStamp = query.toString().split("t=")[1]
+ bundle.putLong("timeStamp", timeStamp.toLong())
+ }
+
+ bundle.putString("videoId", videoId)
val frag = PlayerFragment()
frag.arguments = bundle
+
supportFragmentManager.beginTransaction()
.remove(PlayerFragment())
.commit()
@@ -262,13 +401,36 @@ class MainActivity : AppCompatActivity() {
}, 100)
}
+ private fun loadChannel(
+ channelId: String? = null,
+ channelName: String? = null
+ ) {
+ Log.i(TAG, "Uri Type: Channel")
+
+ val bundle = if (channelId != null) bundleOf("channel_id" to channelId)
+ else bundleOf("channel_name" to channelName)
+ navController.navigate(R.id.channelFragment, bundle)
+ }
+
+ private fun loadPlaylist(playlistId: String) {
+ Log.i(TAG, "Uri Type: Playlist")
+
+ val bundle = bundleOf("playlist_id" to playlistId)
+ navController.navigate(R.id.playlistFragment, bundle)
+ }
+
override fun onBackPressed() {
+ // remove focus from search
+ removeSearchFocus()
+ navController.popBackStack(R.id.searchFragment, false)
+
if (binding.mainMotionLayout.progress == 0F) {
try {
minimizePlayer()
} catch (e: Exception) {
if (navController.currentDestination?.id == startFragmentId) {
- super.onBackPressed()
+ // close app
+ moveTaskToBack(true)
} else {
navController.popBackStack()
}
@@ -292,7 +454,9 @@ class MainActivity : AppCompatActivity() {
enableTransition(R.id.yt_transition, true)
}
findViewById(R.id.linLayout).visibility = View.VISIBLE
- Globals.isFullScreen = false
+ Globals.IS_FULL_SCREEN = false
+ requestedOrientation = if (autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
+ else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -330,9 +494,13 @@ class MainActivity : AppCompatActivity() {
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
)
}
+
+ window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
}
private fun unsetFullscreen() {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
@@ -358,12 +526,3 @@ class MainActivity : AppCompatActivity() {
}
}
}
-
-fun Fragment.hideKeyboard() {
- view?.let { activity?.hideKeyboard(it) }
-}
-
-fun Context.hideKeyboard(view: View) {
- val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
- inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
-}
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 53f08a650..e60ae1ee4 100644
--- a/app/src/main/java/com/github/libretube/activities/NoInternetActivity.kt
+++ b/app/src/main/java/com/github/libretube/activities/NoInternetActivity.kt
@@ -2,18 +2,17 @@ package com.github.libretube.activities
import android.content.Intent
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.R
import com.github.libretube.databinding.ActivityNointernetBinding
+import com.github.libretube.extensions.BaseActivity
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.snackbar.Snackbar
-class NoInternetActivity : AppCompatActivity() {
+class NoInternetActivity : BaseActivity() {
private lateinit var binding: ActivityNointernetBinding
override fun onCreate(savedInstanceState: Bundle?) {
- ThemeHelper.updateTheme(this)
super.onCreate(savedInstanceState)
binding = ActivityNointernetBinding.inflate(layoutInflater)
diff --git a/app/src/main/java/com/github/libretube/activities/RouterActivity.kt b/app/src/main/java/com/github/libretube/activities/RouterActivity.kt
index a90e218da..b9e981b50 100644
--- a/app/src/main/java/com/github/libretube/activities/RouterActivity.kt
+++ b/app/src/main/java/com/github/libretube/activities/RouterActivity.kt
@@ -5,11 +5,11 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
-import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.R
+import com.github.libretube.extensions.BaseActivity
import com.github.libretube.util.ThemeHelper
-class RouterActivity : AppCompatActivity() {
+class RouterActivity : BaseActivity() {
val TAG = "RouterActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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 5adea738a..e33b0019f 100644
--- a/app/src/main/java/com/github/libretube/activities/SettingsActivity.kt
+++ b/app/src/main/java/com/github/libretube/activities/SettingsActivity.kt
@@ -1,32 +1,19 @@
package com.github.libretube.activities
-import android.os.Build
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.databinding.ActivitySettingsBinding
+import com.github.libretube.extensions.BaseActivity
import com.github.libretube.preferences.MainSettings
-import com.github.libretube.util.ThemeHelper
-class SettingsActivity : AppCompatActivity() {
+class SettingsActivity : BaseActivity() {
val TAG = "SettingsActivity"
lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
- ThemeHelper.updateTheme(this)
-
- // apply the theme for the preference dialogs
- setTheme(R.style.MaterialAlertDialog)
-
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- overridePendingTransition(50, 50)
- }
- binding.root.alpha = 0F
- binding.root.animate().alpha(1F).duration = 300
setContentView(binding.root)
@@ -43,16 +30,18 @@ class SettingsActivity : AppCompatActivity() {
}
override fun onBackPressed() {
- if (Globals.isCurrentViewMainSettings) {
- super.onBackPressed()
- finishAndRemoveTask()
- } else {
- Globals.isCurrentViewMainSettings = true
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.settings, MainSettings())
- .commit()
- changeTopBarText(getString(R.string.settings))
+ when (supportFragmentManager.findFragmentById(R.id.settings)) {
+ is MainSettings -> {
+ super.onBackPressed()
+ finishAndRemoveTask()
+ }
+ else -> {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, MainSettings())
+ .commit()
+ changeTopBarText(getString(R.string.settings))
+ }
}
}
diff --git a/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt b/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt
index ed5b83e58..3574d9117 100644
--- a/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/ChannelAdapter.kt
@@ -1,19 +1,18 @@
package com.github.libretube.adapters
-import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
-import com.github.libretube.R
-import com.github.libretube.databinding.VideoChannelRowBinding
+import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog
-import com.github.libretube.fragments.PlayerFragment
import com.github.libretube.obj.StreamItem
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.formatShort
-import com.squareup.picasso.Picasso
+import com.github.libretube.util.setWatchProgressLength
+import com.github.libretube.util.toID
class ChannelAdapter(
private val videoFeed: MutableList,
@@ -26,47 +25,39 @@ class ChannelAdapter(
}
fun updateItems(newItems: List) {
+ val feedSize = videoFeed.size
videoFeed.addAll(newItems)
- notifyDataSetChanged()
+ notifyItemRangeInserted(feedSize, newItems.size)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
- val binding = VideoChannelRowBinding.inflate(layoutInflater, parent, false)
+ val binding = VideoRowBinding.inflate(layoutInflater, parent, false)
return ChannelViewHolder(binding)
}
override fun onBindViewHolder(holder: ChannelViewHolder, position: Int) {
val trending = videoFeed[position]
holder.binding.apply {
- channelDescription.text = trending.title
- channelViews.text =
+ videoTitle.text = trending.title
+ videoInfo.text =
trending.views.formatShort() + " • " +
DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
- channelDuration.text =
+ thumbnailDuration.text =
DateUtils.formatElapsedTime(trending.duration!!)
- Picasso.get().load(trending.thumbnail).into(channelThumbnail)
+ ConnectionHelper.loadImage(trending.thumbnail, thumbnail)
root.setOnClickListener {
- var bundle = Bundle()
- bundle.putString("videoId", trending.url!!.replace("/watch?v=", ""))
- var frag = PlayerFragment()
- frag.arguments = bundle
- val activity = root.context as AppCompatActivity
- activity.supportFragmentManager.beginTransaction()
- .remove(PlayerFragment())
- .commit()
- activity.supportFragmentManager.beginTransaction()
- .replace(R.id.container, frag)
- .commitNow()
+ NavigationHelper.navigateVideo(root.context, trending.url)
}
+ val videoId = trending.url.toID()
root.setOnLongClickListener {
- val videoId = trending.url!!.replace("/watch?v=", "")
- VideoOptionsDialog(videoId, root.context)
- .show(childFragmentManager, VideoOptionsDialog.TAG)
+ VideoOptionsDialog(videoId)
+ .show(childFragmentManager, VideoOptionsDialog::class.java.name)
true
}
+ watchProgress.setWatchProgressLength(videoId, trending.duration!!)
}
}
}
-class ChannelViewHolder(val binding: VideoChannelRowBinding) : RecyclerView.ViewHolder(binding.root)
+class ChannelViewHolder(val binding: VideoRowBinding) : RecyclerView.ViewHolder(binding.root)
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 6a28be080..dba75dfc4 100644
--- a/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/ChaptersAdapter.kt
@@ -1,18 +1,21 @@
package com.github.libretube.adapters
+import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.ChapterColumnBinding
import com.github.libretube.obj.ChapterSegment
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.ThemeHelper
import com.google.android.exoplayer2.ExoPlayer
-import com.squareup.picasso.Picasso
class ChaptersAdapter(
private val chapters: List,
private val exoPlayer: ExoPlayer
) : RecyclerView.Adapter() {
val TAG = "ChaptersAdapter"
+ private var selectedPosition = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
@@ -23,16 +26,30 @@ class ChaptersAdapter(
override fun onBindViewHolder(holder: ChaptersViewHolder, position: Int) {
val chapter = chapters[position]
holder.binding.apply {
- Picasso.get().load(chapter.image).fit().centerCrop().into(chapterImage)
+ ConnectionHelper.loadImage(chapter.image, chapterImage)
chapterTitle.text = chapter.title
+ if (selectedPosition == position) {
+ // get the color for highlighted controls
+ val color =
+ ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
+ chapterLL.setBackgroundColor(color)
+ } else chapterLL.setBackgroundColor(Color.TRANSPARENT)
root.setOnClickListener {
+ updateSelectedPosition(position)
val chapterStart = chapter.start!! * 1000 // s -> ms
exoPlayer.seekTo(chapterStart)
}
}
}
+ fun updateSelectedPosition(newPosition: Int) {
+ val oldPosition = selectedPosition
+ selectedPosition = newPosition
+ notifyItemChanged(oldPosition)
+ notifyItemChanged(newPosition)
+ }
+
override fun getItemCount(): Int {
return chapters.size
}
diff --git a/app/src/main/java/com/github/libretube/adapters/CommentsAdapter.kt b/app/src/main/java/com/github/libretube/adapters/CommentsAdapter.kt
index cbac097ad..688f33514 100644
--- a/app/src/main/java/com/github/libretube/adapters/CommentsAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/CommentsAdapter.kt
@@ -5,18 +5,16 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.CommentsRowBinding
import com.github.libretube.obj.Comment
import com.github.libretube.obj.CommentsPage
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
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
@@ -53,7 +51,7 @@ class CommentsAdapter(
" • " + comment.commentedTime.toString()
commentText.text =
comment.commentText.toString()
- Picasso.get().load(comment.thumbnail).fit().centerCrop().into(commentorImage)
+ ConnectionHelper.loadImage(comment.thumbnail, commentorImage)
likesTextView.text =
comment.likeCount?.toLong().formatShort()
if (comment.verified == true) {
@@ -66,19 +64,7 @@ class CommentsAdapter(
heartedImageView.visibility = View.VISIBLE
}
commentorImage.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to comment.commentorUrl)
- activity.navController.navigate(R.id.channelFragment, bundle)
- try {
- val mainMotionLayout =
- activity.findViewById(R.id.mainMotionLayout)
- if (mainMotionLayout.progress == 0.toFloat()) {
- mainMotionLayout.transitionToEnd()
- activity.findViewById(R.id.playerMotionLayout)
- .transitionToEnd()
- }
- } catch (e: Exception) {
- }
+ NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
}
repliesRecView.layoutManager = LinearLayoutManager(root.context)
val repliesAdapter = RepliesAdapter(CommentsPage().comments)
diff --git a/app/src/main/java/com/github/libretube/adapters/PlaylistAdapter.kt b/app/src/main/java/com/github/libretube/adapters/PlaylistAdapter.kt
index d6ec9bea0..67db0cdcc 100644
--- a/app/src/main/java/com/github/libretube/adapters/PlaylistAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/PlaylistAdapter.kt
@@ -1,24 +1,23 @@
package com.github.libretube.adapters
import android.app.Activity
-import android.os.Bundle
-import android.text.format.DateUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
-import com.github.libretube.R
import com.github.libretube.databinding.PlaylistRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog
-import com.github.libretube.fragments.PlayerFragment
+import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.obj.PlaylistId
import com.github.libretube.obj.StreamItem
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance
-import com.squareup.picasso.Picasso
+import com.github.libretube.util.setWatchProgressLength
+import com.github.libretube.util.toID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -54,69 +53,46 @@ class PlaylistAdapter(
holder.binding.apply {
playlistTitle.text = streamItem.title
playlistDescription.text = streamItem.uploaderName
- playlistDuration.text = DateUtils.formatElapsedTime(streamItem.duration!!)
- Picasso.get().load(streamItem.thumbnail).into(playlistThumbnail)
+ thumbnailDuration.setFormattedDuration(streamItem.duration!!)
+ ConnectionHelper.loadImage(streamItem.thumbnail, playlistThumbnail)
root.setOnClickListener {
- var bundle = Bundle()
- bundle.putString("videoId", streamItem.url!!.replace("/watch?v=", ""))
- bundle.putString("playlistId", playlistId)
- var frag = PlayerFragment()
- frag.arguments = bundle
- val activity = root.context as AppCompatActivity
- activity.supportFragmentManager.beginTransaction()
- .remove(PlayerFragment())
- .commit()
- activity.supportFragmentManager.beginTransaction()
- .replace(R.id.container, frag)
- .commitNow()
+ NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)
}
+ val videoId = streamItem.url.toID()
root.setOnLongClickListener {
- val videoId = streamItem.url!!.replace("/watch?v=", "")
- VideoOptionsDialog(videoId, root.context)
- .show(childFragmentManager, VideoOptionsDialog.TAG)
+ VideoOptionsDialog(videoId)
+ .show(childFragmentManager, VideoOptionsDialog::class.java.name)
true
}
if (isOwner) {
deletePlaylist.visibility = View.VISIBLE
deletePlaylist.setOnClickListener {
- val token = PreferenceHelper.getToken(root.context)
- removeFromPlaylist(token, position)
+ removeFromPlaylist(position)
}
}
+ watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
}
}
- private fun removeFromPlaylist(token: String, position: Int) {
- fun run() {
- CoroutineScope(Dispatchers.IO).launch {
- val response = try {
- RetrofitInstance.authApi.removeFromPlaylist(
- token,
- PlaylistId(playlistId = playlistId, index = position)
- )
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection")
- return@launch
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response")
- return@launch
- } finally {
- }
- try {
- if (response.message == "ok") {
- Log.d(TAG, "deleted!")
- videoFeed.removeAt(position)
- // FIXME: This needs to run on UI thread?
- activity.runOnUiThread { notifyDataSetChanged() }
- }
- } catch (e: Exception) {
- Log.e(TAG, e.toString())
- }
+ fun removeFromPlaylist(position: Int) {
+ videoFeed.removeAt(position)
+ activity.runOnUiThread { notifyDataSetChanged() }
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ RetrofitInstance.authApi.removeFromPlaylist(
+ PreferenceHelper.getToken(),
+ PlaylistId(playlistId = playlistId, index = position)
+ )
+ } catch (e: IOException) {
+ println(e)
+ Log.e(TAG, "IOException, you might not have internet connection")
+ return@launch
+ } catch (e: HttpException) {
+ Log.e(TAG, "HttpException, unexpected response")
+ return@launch
}
}
- run()
}
}
diff --git a/app/src/main/java/com/github/libretube/adapters/PlaylistsAdapter.kt b/app/src/main/java/com/github/libretube/adapters/PlaylistsAdapter.kt
index 7818583e9..37e633215 100644
--- a/app/src/main/java/com/github/libretube/adapters/PlaylistsAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/PlaylistsAdapter.kt
@@ -4,17 +4,18 @@ import android.app.Activity
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
-import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.PlaylistsRowBinding
+import com.github.libretube.dialogs.PlaylistOptionsDialog
import com.github.libretube.obj.PlaylistId
import com.github.libretube.obj.Playlists
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -23,6 +24,7 @@ import java.io.IOException
class PlaylistsAdapter(
private val playlists: MutableList,
+ private val childFragmentManager: FragmentManager,
private val activity: Activity
) : RecyclerView.Adapter() {
val TAG = "PlaylistsAdapter"
@@ -45,11 +47,12 @@ class PlaylistsAdapter(
override fun onBindViewHolder(holder: PlaylistsViewHolder, position: Int) {
val playlist = playlists[position]
holder.binding.apply {
- Picasso.get().load(playlist.thumbnail).into(playlistThumbnail)
// set imageview drawable as empty playlist if imageview empty
- if (playlistThumbnail.drawable == null) {
+ if (playlist.thumbnail!!.split("/").size <= 4) {
playlistThumbnail.setImageResource(R.drawable.ic_empty_playlist)
playlistThumbnail.setBackgroundColor(R.attr.colorSurface)
+ } else {
+ ConnectionHelper.loadImage(playlist.thumbnail, playlistThumbnail)
}
playlistTitle.text = playlist.name
deletePlaylist.setOnClickListener {
@@ -57,27 +60,38 @@ class PlaylistsAdapter(
builder.setTitle(R.string.deletePlaylist)
builder.setMessage(R.string.areYouSure)
builder.setPositiveButton(R.string.yes) { _, _ ->
- val token = PreferenceHelper.getToken(root.context)
- deletePlaylist(playlist.id!!, token, position)
- }
- builder.setNegativeButton(R.string.cancel) { _, _ ->
+ PreferenceHelper.getToken()
+ deletePlaylist(playlist.id!!, position)
}
+ builder.setNegativeButton(R.string.cancel, null)
builder.show()
}
root.setOnClickListener {
- // playlists clicked
- val activity = root.context as MainActivity
- val bundle = bundleOf("playlist_id" to playlist.id)
- activity.navController.navigate(R.id.playlistFragment, bundle)
+ NavigationHelper.navigatePlaylist(root.context, playlist.id, true)
+ }
+
+ root.setOnLongClickListener {
+ val playlistOptionsDialog = PlaylistOptionsDialog(
+ playlistId = playlist.id!!,
+ isOwner = true
+ )
+ playlistOptionsDialog.show(
+ childFragmentManager,
+ PlaylistOptionsDialog::class.java.name
+ )
+ true
}
}
}
- private fun deletePlaylist(id: String, token: String, position: Int) {
+ private fun deletePlaylist(id: String, position: Int) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
val response = try {
- RetrofitInstance.authApi.deletePlaylist(token, PlaylistId(id))
+ RetrofitInstance.authApi.deletePlaylist(
+ PreferenceHelper.getToken(),
+ PlaylistId(id)
+ )
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
@@ -88,9 +102,7 @@ class PlaylistsAdapter(
}
try {
if (response.message == "ok") {
- Log.d(TAG, "deleted!")
playlists.removeAt(position)
- // FIXME: This needs to run on UI thread?
activity.runOnUiThread { notifyDataSetChanged() }
}
} catch (e: Exception) {
diff --git a/app/src/main/java/com/github/libretube/adapters/RepliesAdapter.kt b/app/src/main/java/com/github/libretube/adapters/RepliesAdapter.kt
index d53429a03..b76fc5b87 100644
--- a/app/src/main/java/com/github/libretube/adapters/RepliesAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/RepliesAdapter.kt
@@ -3,15 +3,12 @@ package com.github.libretube.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView
-import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.RepliesRowBinding
import com.github.libretube.obj.Comment
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.formatShort
-import com.squareup.picasso.Picasso
class RepliesAdapter(
private val replies: MutableList
@@ -44,7 +41,7 @@ class RepliesAdapter(
" • " + reply.commentedTime.toString()
commentText.text =
reply.commentText.toString()
- Picasso.get().load(reply.thumbnail).fit().centerCrop().into(commentorImage)
+ ConnectionHelper.loadImage(reply.thumbnail, commentorImage)
likesTextView.text =
reply.likeCount?.toLong().formatShort()
if (reply.verified == true) {
@@ -57,19 +54,7 @@ class RepliesAdapter(
heartedImageView.visibility = View.VISIBLE
}
commentorImage.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to reply.commentorUrl)
- activity.navController.navigate(R.id.channelFragment, bundle)
- try {
- val mainMotionLayout =
- activity.findViewById(R.id.mainMotionLayout)
- if (mainMotionLayout.progress == 0.toFloat()) {
- mainMotionLayout.transitionToEnd()
- activity.findViewById(R.id.playerMotionLayout)
- .transitionToEnd()
- }
- } catch (e: Exception) {
- }
+ NavigationHelper.navigateVideo(root.context, reply.commentorUrl)
}
}
}
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 29a14e39d..62213ab41 100644
--- a/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt
@@ -1,32 +1,27 @@
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
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
-import com.github.libretube.databinding.ChannelSearchRowBinding
+import com.github.libretube.databinding.ChannelRowBinding
import com.github.libretube.databinding.PlaylistSearchRowBinding
-import com.github.libretube.databinding.VideoSearchRowBinding
+import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.dialogs.PlaylistOptionsDialog
import com.github.libretube.dialogs.VideoOptionsDialog
-import com.github.libretube.fragments.PlayerFragment
+import com.github.libretube.extensions.setFormattedDuration
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.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
+import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort
-import com.squareup.picasso.Picasso
+import com.github.libretube.util.setWatchProgressLength
+import com.github.libretube.util.toID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import java.io.IOException
class SearchAdapter(
private val searchItems: MutableList,
@@ -49,10 +44,10 @@ class SearchAdapter(
return when (viewType) {
0 -> SearchViewHolder(
- VideoSearchRowBinding.inflate(layoutInflater, parent, false)
+ VideoRowBinding.inflate(layoutInflater, parent, false)
)
1 -> SearchViewHolder(
- ChannelSearchRowBinding.inflate(layoutInflater, parent, false)
+ ChannelRowBinding.inflate(layoutInflater, parent, false)
)
2 -> SearchViewHolder(
PlaylistSearchRowBinding.inflate(layoutInflater, parent, false)
@@ -82,161 +77,101 @@ class SearchAdapter(
}
}
- private fun bindWatch(item: SearchItem, binding: VideoSearchRowBinding) {
+ private fun bindWatch(item: SearchItem, binding: VideoRowBinding) {
binding.apply {
- Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchThumbnail)
- if (item.duration != -1L) {
- searchThumbnailDuration.text = DateUtils.formatElapsedTime(item.duration!!)
- } else {
- searchThumbnailDuration.text = root.context.getString(R.string.live)
- searchThumbnailDuration.setBackgroundColor(R.attr.colorPrimaryDark)
- }
- Picasso.get().load(item.uploaderAvatar).fit().centerCrop().into(searchChannelImage)
- searchDescription.text = item.title
+ ConnectionHelper.loadImage(item.thumbnail, thumbnail)
+ thumbnailDuration.setFormattedDuration(item.duration!!)
+ ConnectionHelper.loadImage(item.uploaderAvatar, channelImage)
+ videoTitle.text = item.title
val viewsString = if (item.views?.toInt() != -1) item.views.formatShort() else ""
val uploadDate = if (item.uploadedDate != null) item.uploadedDate else ""
- searchViews.text =
+ videoInfo.text =
if (viewsString != "" && uploadDate != "") {
"$viewsString • $uploadDate"
} else {
viewsString + uploadDate
}
- searchChannelName.text = item.uploaderName
+ channelName.text = item.uploaderName
root.setOnClickListener {
- val bundle = Bundle()
- bundle.putString("videoId", item.url!!.replace("/watch?v=", ""))
- val frag = PlayerFragment()
- frag.arguments = bundle
- val activity = root.context as AppCompatActivity
- activity.supportFragmentManager.beginTransaction()
- .remove(PlayerFragment())
- .commit()
- activity.supportFragmentManager.beginTransaction()
- .replace(R.id.container, frag)
- .commitNow()
+ NavigationHelper.navigateVideo(root.context, item.url)
}
+ val videoId = item.url.toID()
root.setOnLongClickListener {
- val videoId = item.url!!.replace("/watch?v=", "")
- VideoOptionsDialog(videoId, root.context)
- .show(childFragmentManager, VideoOptionsDialog.TAG)
+ VideoOptionsDialog(videoId)
+ .show(childFragmentManager, VideoOptionsDialog::class.java.name)
true
}
- searchChannelImage.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to item.uploaderUrl)
- activity.navController.navigate(R.id.channelFragment, bundle)
+ channelImage.setOnClickListener {
+ NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
}
+ watchProgress.setWatchProgressLength(videoId, item.duration!!)
}
}
- private fun bindChannel(item: SearchItem, binding: ChannelSearchRowBinding) {
+ private fun bindChannel(item: SearchItem, binding: ChannelRowBinding) {
binding.apply {
- Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchChannelImage)
+ ConnectionHelper.loadImage(item.thumbnail, searchChannelImage)
searchChannelName.text = item.name
searchViews.text = root.context.getString(
R.string.subscribers,
item.subscribers.formatShort()
) + " • " + root.context.getString(R.string.videoCount, item.videos.toString())
root.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to item.url)
- activity.navController.navigate(R.id.channelFragment, bundle)
+ NavigationHelper.navigateChannel(root.context, item.url)
}
- val channelId = item.url?.replace("/channel/", "")!!
- val token = PreferenceHelper.getToken(root.context)
+ val channelId = item.url.toID()
- // only show subscribe button if logged in
- if (token != "") isSubscribed(channelId, token, binding)
+ isSubscribed(channelId, binding)
}
}
- private fun isSubscribed(channelId: String, token: String, binding: ChannelSearchRowBinding) {
- var isSubscribed = false
-
+ private fun isSubscribed(channelId: String, binding: ChannelRowBinding) {
// check whether the user subscribed to the channel
CoroutineScope(Dispatchers.Main).launch {
- val response = try {
- RetrofitInstance.authApi.isSubscribed(
- channelId,
- token
- )
- } catch (e: Exception) {
- return@launch
- }
+ var isSubscribed = SubscriptionHelper.isSubscribed(channelId)
// if subscribed change text to unsubscribe
- if (response.subscribed == true) {
- isSubscribed = true
+ if (isSubscribed == true) {
binding.searchSubButton.text = binding.root.context.getString(R.string.unsubscribe)
}
// make sub button visible and set the on click listeners to (un)subscribe
- if (response.subscribed != null) {
- binding.searchSubButton.visibility = View.VISIBLE
+ if (isSubscribed == null) return@launch
+ binding.searchSubButton.visibility = View.VISIBLE
- binding.searchSubButton.setOnClickListener {
- if (!isSubscribed) {
- subscribe(token, channelId)
- binding.searchSubButton.text =
- binding.root.context.getString(R.string.unsubscribe)
- isSubscribed = true
- } else {
- unsubscribe(token, channelId)
- binding.searchSubButton.text =
- binding.root.context.getString(R.string.subscribe)
- isSubscribed = false
- }
+ binding.searchSubButton.setOnClickListener {
+ if (isSubscribed == false) {
+ SubscriptionHelper.subscribe(channelId)
+ binding.searchSubButton.text =
+ binding.root.context.getString(R.string.unsubscribe)
+ isSubscribed = true
+ } else {
+ SubscriptionHelper.unsubscribe(channelId)
+ binding.searchSubButton.text =
+ binding.root.context.getString(R.string.subscribe)
+ isSubscribed = false
}
}
}
}
- private fun subscribe(token: String, channelId: String) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- RetrofitInstance.authApi.subscribe(
- token,
- Subscribe(channelId)
- )
- } catch (e: Exception) {
- return@launch
- }
- }
- }
-
- private fun unsubscribe(token: String, channelId: String) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- RetrofitInstance.authApi.unsubscribe(
- token,
- Subscribe(channelId)
- )
- } catch (e: IOException) {
- return@launch
- }
- }
- }
-
private fun bindPlaylist(item: SearchItem, binding: PlaylistSearchRowBinding) {
binding.apply {
- Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchThumbnail)
+ ConnectionHelper.loadImage(item.thumbnail, searchThumbnail)
if (item.videos?.toInt() != -1) searchPlaylistNumber.text = item.videos.toString()
searchDescription.text = item.name
searchName.text = item.uploaderName
if (item.videos?.toInt() != -1) {
- searchPlaylistNumber.text =
+ searchPlaylistVideos.text =
root.context.getString(R.string.videoCount, item.videos.toString())
}
root.setOnClickListener {
- // playlist clicked
- val activity = root.context as MainActivity
- val bundle = bundleOf("playlist_id" to item.url)
- activity.navController.navigate(R.id.playlistFragment, bundle)
+ NavigationHelper.navigatePlaylist(root.context, item.url, false)
}
root.setOnLongClickListener {
- val playlistId = item.url!!.replace("/playlist?list=", "")
- PlaylistOptionsDialog(playlistId, false, root.context)
- .show(childFragmentManager, "PlaylistOptionsDialog")
+ val playlistId = item.url!!.toID()
+ PlaylistOptionsDialog(playlistId, false)
+ .show(childFragmentManager, PlaylistOptionsDialog::class.java.name)
true
}
}
@@ -244,15 +179,15 @@ class SearchAdapter(
}
class SearchViewHolder : RecyclerView.ViewHolder {
- var videoRowBinding: VideoSearchRowBinding? = null
- var channelRowBinding: ChannelSearchRowBinding? = null
+ var videoRowBinding: VideoRowBinding? = null
+ var channelRowBinding: ChannelRowBinding? = null
var playlistRowBinding: PlaylistSearchRowBinding? = null
- constructor(binding: VideoSearchRowBinding) : super(binding.root) {
+ constructor(binding: VideoRowBinding) : super(binding.root) {
videoRowBinding = binding
}
- constructor(binding: ChannelSearchRowBinding) : super(binding.root) {
+ constructor(binding: ChannelRowBinding) : super(binding.root) {
channelRowBinding = binding
}
diff --git a/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt
index 994e4caf4..bc3ac4384 100644
--- a/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt
@@ -1,19 +1,15 @@
package com.github.libretube.adapters
-import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
-import android.widget.EditText
+import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.SearchhistoryRowBinding
-import com.github.libretube.fragments.SearchFragment
import com.github.libretube.preferences.PreferenceHelper
class SearchHistoryAdapter(
- private val context: Context,
private var historyList: List,
- private val editText: EditText,
- private val searchFragment: SearchFragment
+ private val searchView: SearchView
) :
RecyclerView.Adapter() {
@@ -28,19 +24,18 @@ class SearchHistoryAdapter(
}
override fun onBindViewHolder(holder: SearchHistoryViewHolder, position: Int) {
- val history = historyList[position]
+ val historyQuery = historyList[position]
holder.binding.apply {
- historyText.text = history
+ historyText.text = historyQuery
deleteHistory.setOnClickListener {
- historyList = historyList - history
- PreferenceHelper.saveHistory(context, historyList)
+ historyList = historyList - historyQuery
+ PreferenceHelper.removeFromSearchHistory(historyQuery)
notifyDataSetChanged()
}
root.setOnClickListener {
- editText.setText(history)
- searchFragment.fetchSearch(history)
+ searchView.setQuery(historyQuery, true)
}
}
}
diff --git a/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt
index fa751abb1..8ab6014c2 100644
--- a/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt
@@ -2,15 +2,13 @@ package com.github.libretube.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
-import android.widget.EditText
+import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.SearchsuggestionRowBinding
-import com.github.libretube.fragments.SearchFragment
class SearchSuggestionsAdapter(
private var suggestionsList: List,
- private var editText: EditText,
- private val searchFragment: SearchFragment
+ private val searchView: SearchView
) :
RecyclerView.Adapter() {
@@ -31,8 +29,7 @@ class SearchSuggestionsAdapter(
holder.binding.apply {
suggestionText.text = suggestion
root.setOnClickListener {
- editText.setText(suggestion)
- searchFragment.fetchSearch(editText.text.toString())
+ searchView.setQuery(suggestion, true)
}
}
}
diff --git a/app/src/main/java/com/github/libretube/adapters/SubscriptionAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SubscriptionAdapter.kt
deleted file mode 100644
index 9ba5513e7..000000000
--- a/app/src/main/java/com/github/libretube/adapters/SubscriptionAdapter.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package com.github.libretube.adapters
-
-import android.os.Bundle
-import android.text.format.DateUtils
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.core.os.bundleOf
-import androidx.fragment.app.FragmentManager
-import androidx.recyclerview.widget.RecyclerView
-import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
-import com.github.libretube.databinding.TrendingRowBinding
-import com.github.libretube.dialogs.VideoOptionsDialog
-import com.github.libretube.fragments.PlayerFragment
-import com.github.libretube.obj.StreamItem
-import com.github.libretube.util.formatShort
-import com.squareup.picasso.Picasso
-
-class SubscriptionAdapter(
- private val videoFeed: List,
- private val childFragmentManager: FragmentManager
-) : RecyclerView.Adapter() {
- private val TAG = "SubscriptionAdapter"
-
- var i = 0
- override fun getItemCount(): Int {
- return i
- }
-
- fun updateItems() {
- i += 10
- if (i > videoFeed.size) {
- i = videoFeed.size
- }
- notifyDataSetChanged()
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
- val layoutInflater = LayoutInflater.from(parent.context)
- val binding = TrendingRowBinding.inflate(layoutInflater, parent, false)
- return SubscriptionViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
- val trending = videoFeed[position]
- holder.binding.apply {
- textViewTitle.text = trending.title
- textViewChannel.text =
- trending.uploaderName + " • " +
- trending.views.formatShort() + " • " +
- DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
- if (trending.duration != -1L) {
- thumbnailDuration.text = DateUtils.formatElapsedTime(trending.duration!!)
- } else {
- thumbnailDuration.text = root.context.getString(R.string.live)
- thumbnailDuration.setBackgroundColor(R.attr.colorPrimaryDark)
- }
- channelImage.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to trending.uploaderUrl)
- activity.navController.navigate(R.id.channelFragment, bundle)
- try {
- val mainMotionLayout =
- activity.findViewById(R.id.mainMotionLayout)
- if (mainMotionLayout.progress == 0.toFloat()) {
- mainMotionLayout.transitionToEnd()
- activity.findViewById(R.id.playerMotionLayout)
- .transitionToEnd()
- }
- } catch (e: Exception) {
- }
- }
- Picasso.get().load(trending.thumbnail).into(thumbnail)
- Picasso.get().load(trending.uploaderAvatar).into(channelImage)
- root.setOnClickListener {
- val bundle = Bundle()
- bundle.putString("videoId", trending.url!!.replace("/watch?v=", ""))
- val frag = PlayerFragment()
- frag.arguments = bundle
- val activity = root.context as AppCompatActivity
- activity.supportFragmentManager.beginTransaction()
- .remove(PlayerFragment())
- .commit()
- activity.supportFragmentManager.beginTransaction()
- .replace(R.id.container, frag)
- .commitNow()
- }
- root.setOnLongClickListener {
- val videoId = trending.url!!.replace("/watch?v=", "")
- VideoOptionsDialog(videoId, root.context)
- .show(childFragmentManager, VideoOptionsDialog.TAG)
- true
- }
- }
- }
-}
-
-class SubscriptionViewHolder(val binding: TrendingRowBinding) :
- RecyclerView.ViewHolder(binding.root)
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 6def52cb1..ef2ff27a1 100644
--- a/app/src/main/java/com/github/libretube/adapters/SubscriptionChannelAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/SubscriptionChannelAdapter.kt
@@ -1,30 +1,20 @@
package com.github.libretube.adapters
-import android.content.Context
-import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
-import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
-import com.github.libretube.obj.Subscribe
import com.github.libretube.obj.Subscription
-import com.github.libretube.preferences.PreferenceHelper
-import com.github.libretube.util.RetrofitInstance
-import com.squareup.picasso.Picasso
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
+import com.github.libretube.util.SubscriptionHelper
+import com.github.libretube.util.toID
class SubscriptionChannelAdapter(private val subscriptions: MutableList) :
RecyclerView.Adapter() {
val TAG = "SubChannelAdapter"
- private var subscribed = true
- private var isLoading = false
-
override fun getItemCount(): Int {
return subscriptions.size
}
@@ -38,68 +28,29 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList,
- private val childFragmentManager: FragmentManager
-) : RecyclerView.Adapter() {
+ private val streamItems: List,
+ private val childFragmentManager: FragmentManager,
+ private val showAllAtOne: Boolean = true
+) : RecyclerView.Adapter() {
private val TAG = "TrendingAdapter"
+ var index = 10
+
override fun getItemCount(): Int {
- return videoFeed.size
+ return if (showAllAtOne) streamItems.size
+ else if (index >= streamItems.size) streamItems.size - 1
+ else index
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrendingViewHolder {
+ fun updateItems() {
+ index += 10
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TrendingRowBinding.inflate(layoutInflater, parent, false)
- return TrendingViewHolder(binding)
+ return SubscriptionViewHolder(binding)
}
- override fun onBindViewHolder(holder: TrendingViewHolder, position: Int) {
- val trending = videoFeed[position]
+ override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
+ val trending = streamItems[position]
holder.binding.apply {
textViewTitle.text = trending.title
textViewChannel.text =
trending.uploaderName + " • " +
trending.views.formatShort() + " • " +
DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
- if (trending.duration != -1L) {
- thumbnailDuration.text = DateUtils.formatElapsedTime(trending.duration!!)
- } else {
- thumbnailDuration.text = root.context.getString(R.string.live)
- thumbnailDuration.setBackgroundColor(R.attr.colorPrimaryDark)
- }
+ thumbnailDuration.setFormattedDuration(trending.duration!!)
channelImage.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to trending.uploaderUrl)
- activity.navController.navigate(R.id.channelFragment, bundle)
- try {
- val mainMotionLayout =
- activity.findViewById(R.id.mainMotionLayout)
- if (mainMotionLayout.progress == 0.toFloat()) {
- mainMotionLayout.transitionToEnd()
- activity.findViewById(R.id.playerMotionLayout)
- .transitionToEnd()
- }
- } catch (e: Exception) {
- }
+ NavigationHelper.navigateChannel(root.context, trending.uploaderUrl)
}
- if (trending.thumbnail!!.isNotEmpty()) {
- Picasso.get().load(trending.thumbnail).into(thumbnail)
- }
- if (trending.uploaderAvatar!!.isNotEmpty()) {
- Picasso.get().load(trending.uploaderAvatar).into(channelImage)
- }
-
+ ConnectionHelper.loadImage(trending.thumbnail, thumbnail)
+ ConnectionHelper.loadImage(trending.uploaderAvatar, channelImage)
root.setOnClickListener {
- var bundle = Bundle()
- bundle.putString("videoId", trending.url!!.replace("/watch?v=", ""))
- var frag = PlayerFragment()
- frag.arguments = bundle
- val activity = root.context as AppCompatActivity
- activity.supportFragmentManager.beginTransaction()
- .remove(PlayerFragment())
- .commit()
- activity.supportFragmentManager.beginTransaction()
- .replace(R.id.container, frag)
- .commitNow()
+ NavigationHelper.navigateVideo(root.context, trending.url)
}
+ val videoId = trending.url!!.toID()
root.setOnLongClickListener {
- val videoId = trending.url!!.replace("/watch?v=", "")
- VideoOptionsDialog(videoId, root.context)
- .show(childFragmentManager, VideoOptionsDialog.TAG)
+ VideoOptionsDialog(videoId)
+ .show(childFragmentManager, VideoOptionsDialog::class.java.name)
true
}
+ watchProgress.setWatchProgressLength(videoId, trending.duration!!)
}
}
}
-class TrendingViewHolder(val binding: TrendingRowBinding) : RecyclerView.ViewHolder(binding.root)
+class SubscriptionViewHolder(val binding: TrendingRowBinding) :
+ RecyclerView.ViewHolder(binding.root)
diff --git a/app/src/main/java/com/github/libretube/adapters/WatchHistoryAdapter.kt b/app/src/main/java/com/github/libretube/adapters/WatchHistoryAdapter.kt
index 07ed2e388..f6a63f981 100644
--- a/app/src/main/java/com/github/libretube/adapters/WatchHistoryAdapter.kt
+++ b/app/src/main/java/com/github/libretube/adapters/WatchHistoryAdapter.kt
@@ -1,21 +1,17 @@
package com.github.libretube.adapters
-import android.os.Bundle
-import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
-import com.github.libretube.R
-import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.WatchHistoryRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog
-import com.github.libretube.fragments.PlayerFragment
+import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.obj.WatchHistoryItem
-import com.squareup.picasso.Picasso
+import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.util.ConnectionHelper
+import com.github.libretube.util.NavigationHelper
+import com.github.libretube.util.setWatchProgressLength
class WatchHistoryAdapter(
private val watchHistory: MutableList,
@@ -24,10 +20,10 @@ class WatchHistoryAdapter(
RecyclerView.Adapter() {
private val TAG = "WatchHistoryAdapter"
- fun clear() {
- val size = watchHistory.size
- watchHistory.clear()
- notifyItemRangeRemoved(0, size)
+ fun removeFromWatchHistory(position: Int) {
+ PreferenceHelper.removeFromWatchHistory(position)
+ watchHistory.removeAt(position)
+ notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
@@ -41,45 +37,29 @@ class WatchHistoryAdapter(
holder.binding.apply {
videoTitle.text = video.title
channelName.text = video.uploader
- uploadDate.text = video.uploadDate
- thumbnailDuration.text = DateUtils.formatElapsedTime(video.duration?.toLong()!!)
- Picasso.get().load(video.thumbnailUrl).into(thumbnail)
- Picasso.get().load(video.uploaderAvatar).into(channelImage)
+ videoInfo.text = video.uploadDate
+ thumbnailDuration.setFormattedDuration(video.duration!!)
+ ConnectionHelper.loadImage(video.thumbnailUrl, thumbnail)
+ ConnectionHelper.loadImage(video.uploaderAvatar, channelImage)
channelImage.setOnClickListener {
- val activity = root.context as MainActivity
- val bundle = bundleOf("channel_id" to video.uploaderUrl)
- activity.navController.navigate(R.id.channelFragment, bundle)
- try {
- val mainMotionLayout =
- activity.findViewById(R.id.mainMotionLayout)
- if (mainMotionLayout.progress == 0.toFloat()) {
- mainMotionLayout.transitionToEnd()
- activity.findViewById(R.id.playerMotionLayout)
- .transitionToEnd()
- }
- } catch (e: Exception) {
- }
+ NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
+ }
+
+ deleteBTN.setOnClickListener {
+ removeFromWatchHistory(position)
}
root.setOnClickListener {
- var bundle = Bundle()
- bundle.putString("videoId", video.videoId)
- var frag = PlayerFragment()
- frag.arguments = bundle
- val activity = root.context as AppCompatActivity
- activity.supportFragmentManager.beginTransaction()
- .remove(PlayerFragment())
- .commit()
- activity.supportFragmentManager.beginTransaction()
- .replace(R.id.container, frag)
- .commitNow()
+ NavigationHelper.navigateVideo(root.context, video.videoId)
}
root.setOnLongClickListener {
- VideoOptionsDialog(video.videoId!!, root.context)
- .show(childFragmentManager, VideoOptionsDialog.TAG)
+ VideoOptionsDialog(video.videoId!!)
+ .show(childFragmentManager, VideoOptionsDialog::class.java.name)
true
}
+
+ watchProgress.setWatchProgressLength(video.videoId!!, video.duration)
}
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/AddtoPlaylistDialog.kt b/app/src/main/java/com/github/libretube/dialogs/AddToPlaylistDialog.kt
similarity index 84%
rename from app/src/main/java/com/github/libretube/dialogs/AddtoPlaylistDialog.kt
rename to app/src/main/java/com/github/libretube/dialogs/AddToPlaylistDialog.kt
index 14ab1ef30..772a484c1 100644
--- a/app/src/main/java/com/github/libretube/dialogs/AddtoPlaylistDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/AddToPlaylistDialog.kt
@@ -8,6 +8,7 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
+import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.databinding.DialogAddtoplaylistBinding
import com.github.libretube.obj.PlaylistId
@@ -18,7 +19,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
-class AddtoPlaylistDialog : DialogFragment() {
+class AddToPlaylistDialog : DialogFragment() {
private val TAG = "AddToPlaylistDialog"
private lateinit var binding: DialogAddtoplaylistBinding
@@ -32,7 +33,7 @@ class AddtoPlaylistDialog : DialogFragment() {
// Get the layout inflater
binding = DialogAddtoplaylistBinding.inflate(layoutInflater)
- token = PreferenceHelper.getToken(requireContext())
+ token = PreferenceHelper.getToken()
if (token != "") fetchPlaylists()
@@ -59,24 +60,29 @@ class AddtoPlaylistDialog : DialogFragment() {
return@launchWhenCreated
}
if (response.isNotEmpty()) {
- var names = emptyList().toMutableList()
- for (playlist in response) {
- names.add(playlist.name!!)
- }
+ val names = response.map { it.name }
val arrayAdapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names)
arrayAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
binding.playlistsSpinner.adapter = arrayAdapter
+ if (Globals.SELECTED_PLAYLIST_ID != null) {
+ var selectionIndex = 0
+ response.forEachIndexed { index, playlist ->
+ if (playlist.id == Globals.SELECTED_PLAYLIST_ID) selectionIndex = index
+ }
+ binding.playlistsSpinner.setSelection(selectionIndex)
+ }
runOnUiThread {
binding.addToPlaylist.setOnClickListener {
+ val index = binding.playlistsSpinner.selectedItemPosition
+ Globals.SELECTED_PLAYLIST_ID = response[index].id!!
addToPlaylist(
- response[binding.playlistsSpinner.selectedItemPosition].id!!
+ response[index].id!!
)
}
}
- } else {
}
}
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/CreatePlaylistDialog.kt b/app/src/main/java/com/github/libretube/dialogs/CreatePlaylistDialog.kt
index fe316ed02..cd5fb6889 100644
--- a/app/src/main/java/com/github/libretube/dialogs/CreatePlaylistDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/CreatePlaylistDialog.kt
@@ -33,7 +33,7 @@ class CreatePlaylistDialog : DialogFragment() {
dismiss()
}
- token = PreferenceHelper.getToken(requireContext())
+ token = PreferenceHelper.getToken()
binding.createNewPlaylist.setOnClickListener {
// avoid creating the same playlist multiple times by spamming the button
diff --git a/app/src/main/java/com/github/libretube/dialogs/CustomInstanceDialog.kt b/app/src/main/java/com/github/libretube/dialogs/CustomInstanceDialog.kt
index 07fd1f043..96773bf35 100644
--- a/app/src/main/java/com/github/libretube/dialogs/CustomInstanceDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/CustomInstanceDialog.kt
@@ -41,7 +41,7 @@ class CustomInstanceDialog : DialogFragment() {
URL(customInstance.apiUrl).toURI()
URL(customInstance.frontendUrl).toURI()
- PreferenceHelper.saveCustomInstance(requireContext(), customInstance)
+ PreferenceHelper.saveCustomInstance(customInstance)
activity?.recreate()
dismiss()
} catch (e: Exception) {
diff --git a/app/src/main/java/com/github/libretube/dialogs/DeleteAccountDialog.kt b/app/src/main/java/com/github/libretube/dialogs/DeleteAccountDialog.kt
index 9cb0ad48a..6accb54a4 100644
--- a/app/src/main/java/com/github/libretube/dialogs/DeleteAccountDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/DeleteAccountDialog.kt
@@ -45,7 +45,7 @@ class DeleteAccountDialog : DialogFragment() {
private fun deleteAccount(password: String) {
fun run() {
lifecycleScope.launchWhenCreated {
- val token = PreferenceHelper.getToken(requireContext())
+ val token = PreferenceHelper.getToken()
try {
RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
@@ -57,13 +57,13 @@ class DeleteAccountDialog : DialogFragment() {
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
logout()
val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
}
}
run()
}
private fun logout() {
- PreferenceHelper.setToken(requireContext(), "")
+ PreferenceHelper.setToken("")
}
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/DownloadDialog.kt b/app/src/main/java/com/github/libretube/dialogs/DownloadDialog.kt
index 771bd2c8e..4f2ed94a1 100644
--- a/app/src/main/java/com/github/libretube/dialogs/DownloadDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/DownloadDialog.kt
@@ -1,24 +1,20 @@
package com.github.libretube.dialogs
-import android.Manifest
import android.app.Dialog
import android.content.Intent
-import android.content.pm.PackageManager
-import android.os.Build
import android.os.Bundle
-import android.os.Environment
import android.util.Log
+import android.view.View
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
-import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.DialogDownloadBinding
import com.github.libretube.obj.Streams
import com.github.libretube.services.DownloadService
+import com.github.libretube.util.PermissionHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -36,48 +32,25 @@ class DownloadDialog : DialogFragment() {
return activity?.let {
videoId = arguments?.getString("video_id")!!
- val mainActivity = activity as MainActivity
val builder = MaterialAlertDialogBuilder(it)
binding = DialogDownloadBinding.inflate(layoutInflater)
fetchAvailableSources()
- // request storage permissions if not granted yet
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Log.d("myz", "" + Build.VERSION.SDK_INT)
- if (!Environment.isExternalStorageManager()) {
- ActivityCompat.requestPermissions(
- mainActivity,
- arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.MANAGE_EXTERNAL_STORAGE
- ),
- 1
- ) // permission request code is just an int
- }
- } else {
- if (ActivityCompat.checkSelfPermission(
- requireContext(),
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED ||
- ActivityCompat.checkSelfPermission(
- requireContext(),
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- ActivityCompat.requestPermissions(
- mainActivity,
- arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ),
- 1
- )
- }
- }
+ PermissionHelper.requestReadWrite(requireActivity())
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
+ binding.audioRadio.setOnClickListener {
+ binding.videoSpinner.visibility = View.GONE
+ binding.audioSpinner.visibility = View.VISIBLE
+ }
+
+ binding.videoRadio.setOnClickListener {
+ binding.audioSpinner.visibility = View.GONE
+ binding.videoSpinner.visibility = View.VISIBLE
+ }
+
builder.setView(binding.root)
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
@@ -155,14 +128,15 @@ class DownloadDialog : DialogFragment() {
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
binding.download.setOnClickListener {
- val selectedAudioUrl = audioUrl[binding.audioSpinner.selectedItemPosition]
- val selectedVideoUrl = vidUrl[binding.videoSpinner.selectedItemPosition]
+ val selectedAudioUrl =
+ if (binding.audioRadio.isChecked) audioUrl[binding.audioSpinner.selectedItemPosition] else ""
+ val selectedVideoUrl =
+ if (binding.videoRadio.isChecked) vidUrl[binding.videoSpinner.selectedItemPosition] else ""
val intent = Intent(context, DownloadService::class.java)
- intent.putExtra("videoId", videoId)
+ intent.putExtra("videoName", streams.title)
intent.putExtra("videoUrl", selectedVideoUrl)
intent.putExtra("audioUrl", selectedAudioUrl)
- intent.putExtra("duration", duration)
context?.startService(intent)
dismiss()
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/ErrorDialog.kt b/app/src/main/java/com/github/libretube/dialogs/ErrorDialog.kt
new file mode 100644
index 000000000..cd0ac0b01
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/dialogs/ErrorDialog.kt
@@ -0,0 +1,36 @@
+package com.github.libretube.dialogs
+
+import android.app.Dialog
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Bundle
+import android.widget.Toast
+import androidx.fragment.app.DialogFragment
+import com.github.libretube.R
+import com.github.libretube.preferences.PreferenceHelper
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+class ErrorDialog : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val errorLog = PreferenceHelper.getErrorLog()
+ // reset the error log
+ PreferenceHelper.saveErrorLog("")
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.error_occurred)
+ .setMessage(errorLog)
+ .setNegativeButton(R.string.okay, null)
+ .setPositiveButton(R.string.copy) { _, _ ->
+ /**
+ * copy the error log to the clipboard
+ */
+ val clipboard: ClipboardManager =
+ context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(context?.getString(R.string.copied), errorLog)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
+ }
+ .show()
+ }
+}
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 de8a2188c..06704b464 100644
--- a/app/src/main/java/com/github/libretube/dialogs/LoginDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/LoginDialog.kt
@@ -79,11 +79,10 @@ class LoginDialog : DialogFragment() {
Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show()
} else if (response.token != null) {
Toast.makeText(context, R.string.loggedIn, Toast.LENGTH_SHORT).show()
- PreferenceHelper.setToken(requireContext(), response.token!!)
- PreferenceHelper.setUsername(requireContext(), login.username!!)
- val restartDialog = RequireRestartDialog()
- restartDialog.show(parentFragmentManager, "RequireRestartDialog")
+ PreferenceHelper.setToken(response.token!!)
+ PreferenceHelper.setUsername(login.username!!)
dialog?.dismiss()
+ activity?.recreate()
}
}
}
@@ -112,8 +111,8 @@ class LoginDialog : DialogFragment() {
Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show()
} else if (response.token != null) {
Toast.makeText(context, R.string.registered, Toast.LENGTH_SHORT).show()
- PreferenceHelper.setToken(requireContext(), response.token!!)
- PreferenceHelper.setUsername(requireContext(), login.username!!)
+ PreferenceHelper.setToken(response.token!!)
+ PreferenceHelper.setUsername(login.username!!)
dialog?.dismiss()
}
}
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 77520bd33..3fd153692 100644
--- a/app/src/main/java/com/github/libretube/dialogs/LogoutDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/LogoutDialog.kt
@@ -19,13 +19,13 @@ class LogoutDialog : DialogFragment() {
val builder = MaterialAlertDialogBuilder(it)
binding = DialogLogoutBinding.inflate(layoutInflater)
- val user = PreferenceHelper.getUsername(requireContext())
+ val user = PreferenceHelper.getUsername()
binding.user.text =
binding.user.text.toString() + " (" + user + ")"
binding.logout.setOnClickListener {
Toast.makeText(context, R.string.loggedout, Toast.LENGTH_SHORT).show()
- PreferenceHelper.setToken(requireContext(), "")
+ PreferenceHelper.setToken("")
dialog?.dismiss()
activity?.recreate()
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/PlaylistOptionsDialog.kt b/app/src/main/java/com/github/libretube/dialogs/PlaylistOptionsDialog.kt
index bd2124123..eb3750487 100644
--- a/app/src/main/java/com/github/libretube/dialogs/PlaylistOptionsDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/PlaylistOptionsDialog.kt
@@ -1,7 +1,6 @@
package com.github.libretube.dialogs
import android.app.Dialog
-import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.ArrayAdapter
@@ -10,27 +9,31 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.obj.PlaylistId
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.toID
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import retrofit2.HttpException
import java.io.IOException
class PlaylistOptionsDialog(
private val playlistId: String,
- private val isOwner: Boolean,
- context: Context
+ private val isOwner: Boolean
) : DialogFragment() {
val TAG = "PlaylistOptionsDialog"
- private var optionsList = listOf(
- context.getString(R.string.clonePlaylist),
- context.getString(R.string.share)
- )
-
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ // options for the dialog
+ var optionsList = listOf(
+ context?.getString(R.string.playOnBackground),
+ context?.getString(R.string.clonePlaylist),
+ context?.getString(R.string.share)
+ )
+
if (isOwner) {
optionsList = optionsList +
context?.getString(R.string.deletePlaylist)!! -
@@ -49,9 +52,22 @@ class PlaylistOptionsDialog(
)
) { _, which ->
when (optionsList[which]) {
+ // play the playlist in the background
+ context?.getString(R.string.playOnBackground) -> {
+ runBlocking {
+ val playlist =
+ if (isOwner) RetrofitInstance.authApi.getPlaylist(playlistId)
+ else RetrofitInstance.api.getPlaylist(playlistId)
+ BackgroundHelper.playOnBackground(
+ context = requireContext(),
+ videoId = playlist.relatedStreams!![0].url.toID(),
+ playlistId = playlistId
+ )
+ }
+ }
// Clone the playlist to the users Piped account
context?.getString(R.string.clonePlaylist) -> {
- val token = PreferenceHelper.getToken(requireContext())
+ val token = PreferenceHelper.getToken()
if (token != "") {
importPlaylist(token, playlistId)
} else {
@@ -66,10 +82,10 @@ class PlaylistOptionsDialog(
context?.getString(R.string.share) -> {
val shareDialog = ShareDialog(playlistId, true)
// using parentFragmentManager, childFragmentManager doesn't work here
- shareDialog.show(parentFragmentManager, "ShareDialog")
+ shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
}
context?.getString(R.string.deletePlaylist) -> {
- val token = PreferenceHelper.getToken(requireContext())
+ val token = PreferenceHelper.getToken()
deletePlaylist(playlistId, token)
}
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt b/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt
index 1030fe05b..f22b89980 100644
--- a/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/RequireRestartDialog.kt
@@ -18,7 +18,7 @@ class RequireRestartDialog : DialogFragment() {
activity?.recreate()
ThemeHelper.restartMainActivity(requireContext())
}
- .setNegativeButton(R.string.cancel) { _, _ -> }
+ .setNegativeButton(R.string.cancel, null)
.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 ed94c30ef..7509229ce 100644
--- a/app/src/main/java/com/github/libretube/dialogs/ShareDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/ShareDialog.kt
@@ -8,11 +8,13 @@ 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.github.libretube.preferences.PreferenceKeys
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ShareDialog(
private val id: String,
- private val isPlaylist: Boolean
+ private val isPlaylist: Boolean,
+ private val position: Long = 0L
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -38,7 +40,14 @@ class ShareDialog(
else -> instanceUrl
}
val path = if (!isPlaylist) "/watch?v=$id" else "/playlist?list=$id"
- val url = "$host$path"
+ var url = "$host$path"
+ if (PreferenceHelper.getBoolean(
+ PreferenceKeys.SHARE_WITH_TIME_CODE,
+ true
+ )
+ ) {
+ url += "?t=$position"
+ }
val intent = Intent()
intent.apply {
@@ -57,13 +66,12 @@ class ShareDialog(
// get the frontend url if it's a custom instance
private fun getCustomInstanceFrontendUrl(): String {
val instancePref = PreferenceHelper.getString(
- requireContext(),
- "selectInstance",
+ PreferenceKeys.FETCH_INSTANCE,
PIPED_FRONTEND_URL
)
// get the api urls of the other custom instances
- val customInstances = PreferenceHelper.getCustomInstances(requireContext())
+ val customInstances = PreferenceHelper.getCustomInstances()
// return the custom instance frontend url if available
customInstances.forEach { instance ->
diff --git a/app/src/main/java/com/github/libretube/dialogs/UpdateDialog.kt b/app/src/main/java/com/github/libretube/dialogs/UpdateDialog.kt
index 62beaf79c..715400bdd 100644
--- a/app/src/main/java/com/github/libretube/dialogs/UpdateDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/UpdateDialog.kt
@@ -3,43 +3,51 @@ package com.github.libretube.dialogs
import android.app.Dialog
import android.content.Intent
import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.util.Log
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
+import com.github.libretube.services.UpdateService
+import com.github.libretube.update.UpdateInfo
+import com.github.libretube.util.PermissionHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-class UpdateAvailableDialog(
- private val versionTag: String,
- private val updateLink: String
+class UpdateDialog(
+ private val updateInfo: UpdateInfo
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
MaterialAlertDialogBuilder(requireContext())
- .setTitle(context?.getString(R.string.update_available, versionTag))
- .setMessage(context?.getString(R.string.update_available_text))
- .setNegativeButton(context?.getString(R.string.cancel)) { _, _ ->
- dismiss()
- }
+ .setTitle(context?.getString(R.string.update_available, updateInfo.name))
+ .setMessage(context?.getString(R.string.update_now))
+ .setNegativeButton(R.string.cancel, null)
.setPositiveButton(context?.getString(R.string.okay)) { _, _ ->
- val uri = Uri.parse(updateLink)
- val intent = Intent(Intent.ACTION_VIEW).setData(uri)
- startActivity(intent)
+ val downloadUrl = getDownloadUrl(updateInfo)
+ Log.i("downloadUrl", downloadUrl.toString())
+ if (downloadUrl != null) {
+ PermissionHelper.requestReadWrite(requireActivity())
+ val intent = Intent(context, UpdateService::class.java)
+ intent.putExtra("downloadUrl", downloadUrl)
+ context?.startService(intent)
+ } else {
+ val uri = Uri.parse(updateInfo.html_url)
+ val intent = Intent(Intent.ACTION_VIEW).setData(uri)
+ startActivity(intent)
+ }
}
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
-}
-class NoUpdateAvailableDialog() : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return activity?.let {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(context?.getString(R.string.app_uptodate))
- .setMessage(context?.getString(R.string.no_update_available))
- .setPositiveButton(context?.getString(R.string.okay)) { _, _ -> }
- .create()
- } ?: throw IllegalStateException("Activity cannot be null")
+ private fun getDownloadUrl(updateInfo: UpdateInfo): String? {
+ val supportedArchitectures = Build.SUPPORTED_ABIS
+ supportedArchitectures.forEach { arch ->
+ updateInfo.assets?.forEach { asset ->
+ if (asset.name?.contains(arch) == true) return asset.browser_download_url
+ }
+ }
+ return null
}
}
diff --git a/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt b/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt
index f1c5952cc..f10d1d254 100644
--- a/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt
+++ b/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt
@@ -1,14 +1,17 @@
package com.github.libretube.dialogs
import android.app.Dialog
+import android.app.NotificationManager
import android.content.Context
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.fragment.app.DialogFragment
+import com.github.libretube.Globals
+import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.R
import com.github.libretube.preferences.PreferenceHelper
-import com.github.libretube.util.BackgroundMode
+import com.github.libretube.util.BackgroundHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
@@ -16,24 +19,36 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
*
* Needs the [videoId] to load the content from the right video.
*/
-class VideoOptionsDialog(private val videoId: String, context: Context) : DialogFragment() {
- /**
- * List that stores the different menu options. In the future could be add more options here.
- */
- private val optionsList = listOf(
- context.getString(R.string.playOnBackground),
- context.getString(R.string.addToPlaylist),
- context.getString(R.string.share)
- )
+class VideoOptionsDialog(
+ private val videoId: String
+) : DialogFragment() {
+ private val TAG = "VideoOptionsDialog"
/**
* Dialog that returns a [MaterialAlertDialogBuilder] showing a menu of options.
*/
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return MaterialAlertDialogBuilder(requireContext())
- .setNegativeButton(R.string.cancel) { dialog, _ ->
- dialog.dismiss()
+ /**
+ * List that stores the different menu options. In the future could be add more options here.
+ */
+ val optionsList = mutableListOf(
+ context?.getString(R.string.playOnBackground),
+ context?.getString(R.string.addToPlaylist),
+ context?.getString(R.string.share)
+ )
+
+ /**
+ * Check whether the player is running by observing the notification
+ */
+ val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.activeNotifications.forEach {
+ if (it.id == PLAYER_NOTIFICATION_ID) {
+ optionsList += context?.getString(R.string.add_to_queue)
}
+ }
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setNegativeButton(R.string.cancel, null)
.setAdapter(
ArrayAdapter(
requireContext(),
@@ -41,23 +56,23 @@ class VideoOptionsDialog(private val videoId: String, context: Context) : Dialog
optionsList
)
) { _, which ->
- // For now, this checks the position of the option with the position that is in the
- // list. I don't like it, but we will do like this for now.
when (optionsList[which]) {
- // This for example will be the "Background mode" option
+ // Start the background mode
context?.getString(R.string.playOnBackground) -> {
- BackgroundMode.getInstance()
- .playOnBackgroundMode(requireContext(), videoId)
+ BackgroundHelper.playOnBackground(requireContext(), videoId)
}
// Add Video to Playlist Dialog
context?.getString(R.string.addToPlaylist) -> {
- val token = PreferenceHelper.getToken(requireContext())
+ val token = PreferenceHelper.getToken()
if (token != "") {
- val newFragment = AddtoPlaylistDialog()
+ val newFragment = AddToPlaylistDialog()
val bundle = Bundle()
bundle.putString("videoId", videoId)
newFragment.arguments = bundle
- newFragment.show(parentFragmentManager, "AddToPlaylist")
+ newFragment.show(
+ parentFragmentManager,
+ AddToPlaylistDialog::class.java.name
+ )
} else {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
}
@@ -65,14 +80,13 @@ class VideoOptionsDialog(private val videoId: String, context: Context) : Dialog
context?.getString(R.string.share) -> {
val shareDialog = ShareDialog(videoId, false)
// using parentFragmentManager is important here
- shareDialog.show(parentFragmentManager, "ShareDialog")
+ shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
+ }
+ context?.getString(R.string.add_to_queue) -> {
+ Globals.playingQueue += videoId
}
}
}
.show()
}
-
- companion object {
- const val TAG = "VideoOptionsDialog"
- }
}
diff --git a/app/src/main/java/com/github/libretube/extensions/BaseActivity.kt b/app/src/main/java/com/github/libretube/extensions/BaseActivity.kt
new file mode 100644
index 000000000..cae26114a
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/BaseActivity.kt
@@ -0,0 +1,14 @@
+package com.github.libretube.extensions
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.github.libretube.util.ThemeHelper
+
+open class BaseActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ // set the app theme (e.g. Material You)
+ ThemeHelper.updateTheme(this)
+
+ super.onCreate(savedInstanceState)
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/BaseFragment.kt b/app/src/main/java/com/github/libretube/extensions/BaseFragment.kt
new file mode 100644
index 000000000..ded091322
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/BaseFragment.kt
@@ -0,0 +1,10 @@
+package com.github.libretube.extensions
+
+import androidx.fragment.app.Fragment
+
+open class BaseFragment : Fragment() {
+ fun runOnUiThread(action: () -> Unit) {
+ if (!isAdded) return // Fragment not attached to an Activity
+ activity?.runOnUiThread(action)
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/FormatShort.kt b/app/src/main/java/com/github/libretube/extensions/FormatShort.kt
similarity index 100%
rename from app/src/main/java/com/github/libretube/util/FormatShort.kt
rename to app/src/main/java/com/github/libretube/extensions/FormatShort.kt
diff --git a/app/src/main/java/com/github/libretube/extensions/HideKeyboard.kt b/app/src/main/java/com/github/libretube/extensions/HideKeyboard.kt
new file mode 100644
index 000000000..de99f682d
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/HideKeyboard.kt
@@ -0,0 +1,11 @@
+package com.github.libretube.util
+
+import android.app.Activity
+import android.content.Context
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+
+fun Context.hideKeyboard(view: View) {
+ val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/SetFormattedDuration.kt b/app/src/main/java/com/github/libretube/extensions/SetFormattedDuration.kt
new file mode 100644
index 000000000..0551ee066
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/SetFormattedDuration.kt
@@ -0,0 +1,14 @@
+package com.github.libretube.extensions
+
+import android.text.format.DateUtils
+import android.widget.TextView
+import com.github.libretube.R
+
+fun TextView?.setFormattedDuration(duration: Long) {
+ val text = if (duration < 0L) {
+ this!!.setBackgroundColor(R.attr.colorPrimaryDark)
+ this.context.getString(R.string.live)
+ } else if (duration == 0L) this!!.context.getString(R.string.yt_shorts)
+ else DateUtils.formatElapsedTime(duration)
+ this!!.text = text
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/SetWatchProgressLength.kt b/app/src/main/java/com/github/libretube/extensions/SetWatchProgressLength.kt
new file mode 100644
index 000000000..22780a71e
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/SetWatchProgressLength.kt
@@ -0,0 +1,39 @@
+package com.github.libretube.util
+
+import android.view.View
+import android.view.ViewTreeObserver
+import android.widget.LinearLayout
+import com.github.libretube.preferences.PreferenceHelper
+
+/**
+ * shows the already watched time under the video
+ */
+fun View?.setWatchProgressLength(videoId: String, duration: Long) {
+ val view = this!!
+ val positions = PreferenceHelper.getWatchPositions()
+ var newWidth: Long? = null
+ view.getViewTreeObserver()
+ .addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ this@setWatchProgressLength.getViewTreeObserver().removeOnGlobalLayoutListener(this)
+ positions.forEach {
+ if (it.videoId == videoId) {
+ val fullWidth = (parent as LinearLayout).width
+ if (duration != 0L) newWidth =
+ (fullWidth * (it.position / (duration))) / 1000
+ return@forEach
+ }
+ }
+ if (newWidth != null) {
+ val lp = view.layoutParams
+ lp.apply {
+ width = newWidth!!.toInt()
+ }
+ view.layoutParams = lp
+ view.visibility = View.VISIBLE
+ } else {
+ view.visibility = View.GONE
+ }
+ }
+ })
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/ToID.kt b/app/src/main/java/com/github/libretube/extensions/ToID.kt
new file mode 100644
index 000000000..43ff8420d
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/ToID.kt
@@ -0,0 +1,12 @@
+package com.github.libretube.util
+
+/**
+ * format a Piped route to an ID
+ */
+fun Any?.toID(): String {
+ return this!!
+ .toString()
+ .replace("/watch?v=", "") // videos
+ .replace("/channel/", "") // channels
+ .replace("/playlist?list=", "") // playlists
+}
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 ffcf56b7c..5399e75dc 100644
--- a/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/ChannelFragment.kt
@@ -1,39 +1,43 @@
package com.github.libretube.fragments
-import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.adapters.ChannelAdapter
import com.github.libretube.databinding.FragmentChannelBinding
-import com.github.libretube.obj.Subscribe
-import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.extensions.BaseFragment
+import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort
-import com.squareup.picasso.Picasso
+import com.github.libretube.util.toID
import retrofit2.HttpException
import java.io.IOException
-class ChannelFragment : Fragment() {
+class ChannelFragment : BaseFragment() {
private val TAG = "ChannelFragment"
private lateinit var binding: FragmentChannelBinding
private var channelId: String? = null
+ private var channelName: String? = null
+
var nextPage: String? = null
private var channelAdapter: ChannelAdapter? = null
private var isLoading = true
- private var isSubscribed: Boolean = false
+ private var isSubscribed: Boolean? = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
- channelId = it.getString("channel_id")
+ channelId = it.getString("channel_id").toID()
+ channelName = it.getString("channel_name")
+ ?.replace("/c/", "")
+ ?.replace("/user/", "")
}
}
@@ -49,16 +53,13 @@ class ChannelFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- channelId = channelId!!.replace("/channel/", "")
binding.channelName.text = channelId
binding.channelRecView.layoutManager = LinearLayoutManager(context)
val refreshChannel = {
binding.channelRefresh.isRefreshing = true
fetchChannel()
- if (PreferenceHelper.getToken(requireContext()) != "") {
- isSubscribed()
- }
+ isSubscribed()
}
refreshChannel()
binding.channelRefresh.setOnRefreshListener {
@@ -74,92 +75,43 @@ class ChannelFragment : Fragment() {
if (nextPage != null && !isLoading) {
isLoading = true
binding.channelRefresh.isRefreshing = true
- fetchNextPage()
+ fetchChannelNextPage()
}
}
}
}
private fun isSubscribed() {
- @SuppressLint("ResourceAsColor")
- fun run() {
- lifecycleScope.launchWhenCreated {
- val response = try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.isSubscribed(
- channelId!!,
- token
- )
- } catch (e: Exception) {
- Log.e(TAG, e.toString())
- return@launchWhenCreated
+ lifecycleScope.launchWhenCreated {
+ isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
+ if (isSubscribed == null) return@launchWhenCreated
+
+ runOnUiThread {
+ if (isSubscribed == true) {
+ binding.channelSubscribe.text = getString(R.string.unsubscribe)
}
- runOnUiThread {
- if (response.subscribed == true) {
+ binding.channelSubscribe.setOnClickListener {
+ binding.channelSubscribe.text = if (isSubscribed == true) {
+ SubscriptionHelper.unsubscribe(channelId!!)
+ isSubscribed = false
+ getString(R.string.subscribe)
+ } else {
+ SubscriptionHelper.subscribe(channelId!!)
isSubscribed = true
- binding.channelSubscribe.text = getString(R.string.unsubscribe)
- }
- if (response.subscribed != null) {
- binding.channelSubscribe.apply {
- setOnClickListener {
- text = if (isSubscribed) {
- unsubscribe()
- getString(R.string.subscribe)
- } else {
- subscribe()
- getString(R.string.unsubscribe)
- }
- }
- }
+ getString(R.string.unsubscribe)
}
}
}
}
- run()
- }
-
- private fun subscribe() {
- fun run() {
- lifecycleScope.launchWhenCreated {
- try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.subscribe(
- token,
- Subscribe(channelId)
- )
- } catch (e: Exception) {
- Log.e(TAG, e.toString())
- }
- isSubscribed = true
- }
- }
- run()
- }
-
- private fun unsubscribe() {
- fun run() {
- lifecycleScope.launchWhenCreated {
- try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.unsubscribe(
- token,
- Subscribe(channelId)
- )
- } catch (e: Exception) {
- Log.e(TAG, e.toString())
- }
- isSubscribed = false
- }
- }
- run()
}
private fun fetchChannel() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
- RetrofitInstance.api.getChannel(channelId!!)
+ if (channelId != null) RetrofitInstance.api.getChannel(channelId!!)
+ else RetrofitInstance.api.getChannelByName(channelName!!)
} catch (e: IOException) {
binding.channelRefresh.isRefreshing = false
println(e)
@@ -194,8 +146,10 @@ class ChannelFragment : Fragment() {
binding.channelDescription.text = response.description?.trim()
}
- Picasso.get().load(response.bannerUrl).into(binding.channelBanner)
- Picasso.get().load(response.avatarUrl).into(binding.channelImage)
+ ConnectionHelper.loadImage(response.bannerUrl, binding.channelBanner)
+ ConnectionHelper.loadImage(response.avatarUrl, binding.channelImage)
+
+ // recyclerview of the videos by the channel
channelAdapter = ChannelAdapter(
response.relatedStreams!!.toMutableList(),
childFragmentManager
@@ -207,7 +161,7 @@ class ChannelFragment : Fragment() {
run()
}
- private fun fetchNextPage() {
+ private fun fetchChannelNextPage() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
@@ -230,10 +184,4 @@ class ChannelFragment : Fragment() {
}
run()
}
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
}
diff --git a/app/src/main/java/com/github/libretube/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/fragments/HomeFragment.kt
index 832573fab..2a1312e55 100644
--- a/app/src/main/java/com/github/libretube/fragments/HomeFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/HomeFragment.kt
@@ -6,19 +6,20 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
-import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.github.libretube.R
import com.github.libretube.adapters.TrendingAdapter
import com.github.libretube.databinding.FragmentHomeBinding
+import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.RetrofitInstance
import retrofit2.HttpException
import java.io.IOException
-class HomeFragment : Fragment() {
+class HomeFragment : BaseFragment() {
private val TAG = "HomeFragment"
private lateinit var binding: FragmentHomeBinding
private lateinit var region: String
@@ -41,12 +42,11 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val grid = PreferenceHelper.getString(
- requireContext(),
- "grid",
+ PreferenceKeys.GRID_COLUMNS,
resources.getInteger(R.integer.grid_items).toString()
)!!
- val regionPref = PreferenceHelper.getString(requireContext(), "region", "sys")!!
+ val regionPref = PreferenceHelper.getString(PreferenceKeys.REGION, "sys")!!
// get the system default country if auto region selected
region = if (regionPref == "sys") {
@@ -88,10 +88,4 @@ class HomeFragment : Fragment() {
}
run()
}
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
}
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 b5ceea33b..1d306390c 100644
--- a/app/src/main/java/com/github/libretube/fragments/LibraryFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/LibraryFragment.kt
@@ -6,21 +6,23 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
-import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.adapters.PlaylistsAdapter
import com.github.libretube.databinding.FragmentLibraryBinding
import com.github.libretube.dialogs.CreatePlaylistDialog
+import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance
import retrofit2.HttpException
import java.io.IOException
-class LibraryFragment : Fragment() {
+class LibraryFragment : BaseFragment() {
private val TAG = "LibraryFragment"
lateinit var token: String
@@ -43,12 +45,12 @@ class LibraryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- binding.playlistRecView.layoutManager = LinearLayoutManager(view.context)
- token = PreferenceHelper.getToken(requireContext())
+ binding.playlistRecView.layoutManager = LinearLayoutManager(requireContext())
+ token = PreferenceHelper.getToken()
// hide watch history button of history disabled
val watchHistoryEnabled =
- PreferenceHelper.getBoolean(requireContext(), "watch_history_toggle", true)
+ PreferenceHelper.getBoolean(PreferenceKeys.WATCH_HISTORY_TOGGLE, true)
if (!watchHistoryEnabled) {
binding.showWatchHistory.visibility = View.GONE
} else {
@@ -58,8 +60,7 @@ class LibraryFragment : Fragment() {
}
if (token != "") {
- binding.boogh.visibility = View.GONE
- binding.textLike.visibility = View.GONE
+ binding.loginOrRegister.visibility = View.GONE
fetchPlaylists()
binding.playlistRefresh.isEnabled = true
binding.playlistRefresh.setOnRefreshListener {
@@ -67,7 +68,7 @@ class LibraryFragment : Fragment() {
}
binding.createPlaylist.setOnClickListener {
val newFragment = CreatePlaylistDialog()
- newFragment.show(childFragmentManager, "Create Playlist")
+ newFragment.show(childFragmentManager, CreatePlaylistDialog::class.java.name)
}
} else {
binding.playlistRefresh.isEnabled = false
@@ -78,7 +79,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 (Globals.isMiniPlayerVisible) 180 else 64
+ layoutParams.bottomMargin = if (Globals.MINI_PLAYER_VISIBLE) 180 else 64
binding.createPlaylist.layoutParams = layoutParams
super.onResume()
}
@@ -102,35 +103,36 @@ class LibraryFragment : Fragment() {
binding.playlistRefresh.isRefreshing = false
}
if (response.isNotEmpty()) {
- runOnUiThread {
- binding.boogh.visibility = View.GONE
- binding.textLike.visibility = View.GONE
- }
+ binding.loginOrRegister.visibility = View.GONE
+
val playlistsAdapter = PlaylistsAdapter(
response.toMutableList(),
+ childFragmentManager,
requireActivity()
)
+
+ // listen for playlists to become deleted
+ playlistsAdapter.registerAdapterDataObserver(object :
+ RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ Log.e(TAG, playlistsAdapter.itemCount.toString())
+ if (playlistsAdapter.itemCount == 0) {
+ binding.loginOrRegister.visibility = View.VISIBLE
+ }
+ super.onChanged()
+ }
+ })
+
binding.playlistRecView.adapter = playlistsAdapter
} else {
runOnUiThread {
- binding.boogh.apply {
- visibility = View.VISIBLE
- setImageResource(R.drawable.ic_list)
- }
- binding.textLike.apply {
- visibility = View.VISIBLE
- text = getString(R.string.emptyList)
- }
+ binding.loginOrRegister.visibility = View.VISIBLE
+ binding.boogh.setImageResource(R.drawable.ic_list)
+ binding.textLike.text = getString(R.string.emptyList)
}
}
}
}
run()
}
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
}
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 b05624a71..8d3f43cdd 100644
--- a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt
@@ -1,13 +1,11 @@
package com.github.libretube.fragments
-import android.annotation.SuppressLint
-import android.app.NotificationManager
+import android.app.ActivityManager
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
@@ -16,11 +14,11 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
-import android.support.v4.media.session.MediaSessionCompat
import android.text.Html
-import android.text.TextUtils
+import android.text.format.DateUtils
import android.util.Log
import android.view.LayoutInflater
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
@@ -29,39 +27,44 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
+import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.activities.MainActivity
-import com.github.libretube.activities.hideKeyboard
import com.github.libretube.adapters.ChaptersAdapter
import com.github.libretube.adapters.CommentsAdapter
import com.github.libretube.adapters.TrendingAdapter
+import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.FragmentPlayerBinding
-import com.github.libretube.dialogs.AddtoPlaylistDialog
+import com.github.libretube.dialogs.AddToPlaylistDialog
import com.github.libretube.dialogs.DownloadDialog
import com.github.libretube.dialogs.ShareDialog
+import com.github.libretube.extensions.BaseFragment
+import com.github.libretube.interfaces.DoubleTapInterface
+import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.obj.ChapterSegment
-import com.github.libretube.obj.PipedStream
-import com.github.libretube.obj.Playlist
import com.github.libretube.obj.Segment
import com.github.libretube.obj.Segments
-import com.github.libretube.obj.SponsorBlockPrefs
-import com.github.libretube.obj.StreamItem
import com.github.libretube.obj.Streams
-import com.github.libretube.obj.Subscribe
import com.github.libretube.preferences.PreferenceHelper
-import com.github.libretube.services.IS_DOWNLOAD_RUNNING
-import com.github.libretube.util.BackgroundMode
+import com.github.libretube.preferences.PreferenceKeys
+import com.github.libretube.services.BackgroundMode
+import com.github.libretube.util.AutoPlayHelper
+import com.github.libretube.util.BackgroundHelper
+import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.CronetHelper
-import com.github.libretube.util.DescriptionAdapter
+import com.github.libretube.util.NowPlayingNotification
+import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort
-import com.github.libretube.views.DoubleClickListener
+import com.github.libretube.util.hideKeyboard
+import com.github.libretube.util.toID
+import com.github.libretube.views.BottomSheetFragment
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer
@@ -71,23 +74,20 @@ import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.cronet.CronetDataSource
-import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
-import com.google.android.exoplayer2.ui.PlayerNotificationManager
+import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.StyledPlayerView
-import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.exoplayer2.video.VideoSize
-import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -97,57 +97,89 @@ import java.io.IOException
import java.util.concurrent.Executors
import kotlin.math.abs
-class PlayerFragment : Fragment() {
+class PlayerFragment : BaseFragment() {
private val TAG = "PlayerFragment"
private lateinit var binding: FragmentPlayerBinding
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
+ private lateinit var doubleTapOverlayBinding: DoubleTapOverlayBinding
+ /**
+ * video information
+ */
private var videoId: String? = null
private var playlistId: String? = null
+ private var isSubscribed: Boolean? = false
+ private var isLive = false
+ private lateinit var streams: Streams
+
+ /**
+ * for the transition
+ */
private var sId: Int = 0
private var eId: Int = 0
- private var paused = false
- private var whichQuality = 0
private var transitioning = false
- private var autoplay = false
- private var isZoomed: Boolean = false
-
- private var isSubscribed: Boolean = false
+ /**
+ * for the comments
+ */
private var commentsAdapter: CommentsAdapter? = null
private var commentsLoaded: Boolean? = false
private var nextPage: String? = null
private var isLoading = true
- private lateinit var exoPlayerView: StyledPlayerView
+
+ /**
+ * for the player
+ */
private lateinit var exoPlayer: ExoPlayer
+ private lateinit var trackSelector: DefaultTrackSelector
private lateinit var segmentData: Segments
- private var relatedStreamsEnabled = true
-
- private var relatedStreams: List? = arrayListOf()
- private var nextStreamId: String? = null
- private var playlistStreamIds: MutableList = arrayListOf()
- private var playlistNextPage: String? = null
-
- private var isPlayerLocked: Boolean = false
-
- private lateinit var mediaSession: MediaSessionCompat
- private lateinit var mediaSessionConnector: MediaSessionConnector
- private lateinit var playerNotification: PlayerNotificationManager
-
- private lateinit var title: String
- private lateinit var uploader: String
- private lateinit var thumbnailUrl: String
private lateinit var chapters: List
- private val sponsorBlockPrefs = SponsorBlockPrefs()
- private lateinit var subtitle: MutableList
+ /**
+ * for the player view
+ */
+ private lateinit var exoPlayerView: StyledPlayerView
+ private var isPlayerLocked: Boolean = false
+ private var subtitle = mutableListOf()
+
+ /**
+ * user preferences
+ */
+ private var token = ""
+ private var relatedStreamsEnabled = true
+ private var autoplayEnabled = false
private var autoRotationEnabled = true
+ private var playbackSpeed = "1F"
+ private var pausePlayerOnScreenOffEnabled = false
+ private var fullscreenOrientationPref = "ratio"
+ private var watchHistoryEnabled = true
+ private var watchPositionsEnabled = true
+ private var useSystemCaptionStyle = true
+ private var seekIncrement = 5L
+ private var videoFormatPreference = "webm"
+ private var defRes = ""
+ private var bufferingGoal = 50000
+ private var defaultSubtitleCode = ""
+ private var sponsorBlockEnabled = true
+ private var sponsorBlockNotifications = true
+ private var skipButtonsEnabled = false
+
+ /**
+ * for autoplay
+ */
+ private var nextStreamId: String? = null
+ private lateinit var autoPlayHelper: AutoPlayHelper
+
+ /**
+ * for the player notification
+ */
+ private lateinit var nowPlayingNotification: NowPlayingNotification
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
- videoId = it.getString("videoId")
+ videoId = it.getString("videoId").toID()
playlistId = it.getString("playlistId")
}
}
@@ -158,57 +190,149 @@ class PlayerFragment : Fragment() {
savedInstanceState: Bundle?
): View {
binding = FragmentPlayerBinding.inflate(layoutInflater, container, false)
+ exoPlayerView = binding.player
playerBinding = binding.player.binding
+ doubleTapOverlayBinding = binding.doubleTapOverlay.binding
+
// Inflate the layout for this fragment
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- hideKeyboard()
+ context?.hideKeyboard(view)
+
+ // clear the playing queue
+ Globals.playingQueue.clear()
+
+ setUserPrefs()
- // 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
+ mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
onConfigurationChanged(resources.configuration)
} else {
// go to portrait mode
mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
+ createExoPlayer()
+ initializeTransitionLayout()
+ initializeOnClickActions()
+ playVideo()
+
+ showBottomBar()
+ }
+
+ /**
+ * somehow the bottom bar is invisible on low screen resolutions, this fixes it
+ */
+ private fun showBottomBar() {
+ if (this::playerBinding.isInitialized && !isPlayerLocked) {
+ playerBinding.exoBottomBar.visibility = View.VISIBLE
+ }
+ Handler(Looper.getMainLooper()).postDelayed(this::showBottomBar, 100)
+ }
+
+ private fun setUserPrefs() {
+ token = PreferenceHelper.getToken()
+
+ // save whether auto rotation is enabled
+ autoRotationEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.AUTO_FULLSCREEN,
+ false
+ )
+
// save whether related streams and autoplay are enabled
- autoplay = PreferenceHelper.getBoolean(
- requireContext(),
- "autoplay",
+ autoplayEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.AUTO_PLAY,
false
)
relatedStreamsEnabled = PreferenceHelper.getBoolean(
- requireContext(),
- "related_streams_toggle",
+ PreferenceKeys.RELATED_STREAMS,
true
)
- setSponsorBlockPrefs()
- createExoPlayer(view)
- initializeTransitionLayout(view)
- playVideo(view)
+ playbackSpeed = PreferenceHelper.getString(
+ PreferenceKeys.PLAYBACK_SPEED,
+ "1"
+ ).replace("F", "") // due to old way to handle it (with float)
+
+ fullscreenOrientationPref = PreferenceHelper.getString(
+ PreferenceKeys.FULLSCREEN_ORIENTATION,
+ "ratio"
+ )
+
+ pausePlayerOnScreenOffEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.PAUSE_ON_SCREEN_OFF,
+ false
+ )
+
+ watchPositionsEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.WATCH_POSITION_TOGGLE,
+ true
+ )
+
+ watchHistoryEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.WATCH_HISTORY_TOGGLE,
+ true
+ )
+
+ useSystemCaptionStyle = PreferenceHelper.getBoolean(
+ PreferenceKeys.SYSTEM_CAPTION_STYLE,
+ true
+ )
+
+ seekIncrement = PreferenceHelper.getString(
+ PreferenceKeys.SEEK_INCREMENT,
+ "5"
+ ).toLong() * 1000
+
+ videoFormatPreference = PreferenceHelper.getString(
+ PreferenceKeys.PLAYER_VIDEO_FORMAT,
+ "webm"
+ )
+
+ defRes = PreferenceHelper.getString(
+ PreferenceKeys.DEFAULT_RESOLUTION,
+ ""
+ )
+
+ bufferingGoal = PreferenceHelper.getString(
+ PreferenceKeys.BUFFERING_GOAL,
+ "50"
+ ).toInt() * 1000
+
+ sponsorBlockEnabled = PreferenceHelper.getBoolean(
+ "sb_enabled_key",
+ true
+ )
+
+ sponsorBlockNotifications = PreferenceHelper.getBoolean(
+ "sb_notifications_key",
+ true
+ )
+
+ defaultSubtitleCode = PreferenceHelper.getString(
+ PreferenceKeys.DEFAULT_SUBTITLE,
+ ""
+ )
+
+ if (defaultSubtitleCode.contains("-")) {
+ defaultSubtitleCode = defaultSubtitleCode.split("-")[0]
+ }
+
+ skipButtonsEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.SKIP_BUTTONS,
+ false
+ )
}
- private fun initializeTransitionLayout(view: View) {
- videoId = videoId!!.replace("/watch?v=", "")
-
+ private fun initializeTransitionLayout() {
val mainActivity = activity as MainActivity
mainActivity.binding.container.visibility = View.VISIBLE
- exoPlayerView = binding.player
-
binding.playerMotionLayout.addTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?,
@@ -238,11 +362,11 @@ class PlayerFragment : Fragment() {
val mainMotionLayout =
mainActivity.binding.mainMotionLayout
if (currentId == eId) {
- Globals.isMiniPlayerVisible = true
+ Globals.MINI_PLAYER_VISIBLE = true
exoPlayerView.useController = false
mainMotionLayout.progress = 1F
} else if (currentId == sId) {
- Globals.isMiniPlayerVisible = false
+ Globals.MINI_PLAYER_VISIBLE = false
exoPlayerView.useController = true
mainMotionLayout.progress = 0F
}
@@ -260,8 +384,176 @@ class PlayerFragment : Fragment() {
binding.playerMotionLayout.progress = 1.toFloat()
binding.playerMotionLayout.transitionToStart()
+ // quitting miniPlayer on single click
+ binding.titleTextView.setOnTouchListener { view, motionEvent ->
+ view.onTouchEvent(motionEvent)
+ if (motionEvent.action == MotionEvent.ACTION_UP) view.performClick()
+ binding.root.onTouchEvent(motionEvent)
+ }
+ binding.titleTextView.setOnClickListener {
+ binding.playerMotionLayout.setTransitionDuration(300)
+ binding.playerMotionLayout.transitionToStart()
+ }
+ }
+
+ private val playerOptionsInterface = object : PlayerOptionsInterface {
+ override fun onAutoplayClicked() {
+ // autoplay options dialog
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.player_autoplay)
+ .setItems(
+ arrayOf(
+ context?.getString(R.string.enabled),
+ context?.getString(R.string.disabled)
+ )
+ ) { _, index ->
+ when (index) {
+ 0 -> autoplayEnabled = true
+ 1 -> autoplayEnabled = false
+ }
+ }
+ .show()
+ }
+
+ override fun onCaptionClicked() {
+ if (!this@PlayerFragment::streams.isInitialized ||
+ streams.subtitles == null ||
+ streams.subtitles!!.isEmpty()
+ ) {
+ Toast.makeText(context, R.string.no_subtitles_available, Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val subtitlesNamesList = mutableListOf(context?.getString(R.string.none)!!)
+ val subtitleCodesList = mutableListOf("")
+ streams.subtitles!!.forEach {
+ subtitlesNamesList += it.name!!
+ subtitleCodesList += it.code!!
+ }
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.captions)
+ .setItems(subtitlesNamesList.toTypedArray()) { _, index ->
+ val newParams = if (index != 0) {
+ // caption selected
+
+ // get the caption name and language
+ val captionLanguage = subtitlesNamesList[index]
+ val captionLanguageCode = subtitleCodesList[index]
+
+ // select the new caption preference
+ trackSelector.buildUponParameters()
+ .setPreferredTextLanguages(
+ captionLanguage,
+ captionLanguageCode
+ )
+ .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
+ } else {
+ // none selected
+ // disable captions
+ trackSelector.buildUponParameters()
+ .setPreferredTextLanguage("")
+ }
+
+ // set the new caption language
+ trackSelector.setParameters(newParams)
+ }
+ .show()
+ }
+
+ override fun onQualityClicked() {
+ // get the available resolutions
+ val (videosNameArray, videosUrlArray) = getAvailableResolutions()
+
+ // Dialog for quality selection
+ val lastPosition = exoPlayer.currentPosition
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.choose_quality_dialog)
+ .setItems(
+ videosNameArray
+ ) { _, which ->
+ if (
+ videosNameArray[which] == getString(R.string.hls) ||
+ videosNameArray[which] == "LBRY HLS"
+ ) {
+ // no need to merge sources if using hls
+ val mediaItem: MediaItem = MediaItem.Builder()
+ .setUri(videosUrlArray[which])
+ .setSubtitleConfigurations(subtitle)
+ .build()
+ exoPlayer.setMediaItem(mediaItem)
+ } else {
+ val videoUri = videosUrlArray[which]
+ val audioUrl = PlayerHelper.getAudioSource(streams.audioStreams!!)
+ setMediaSource(videoUri, audioUrl)
+ }
+ exoPlayer.seekTo(lastPosition)
+ }
+ .show()
+ }
+
+ override fun onPlaybackSpeedClicked() {
+ val playbackSpeeds = context?.resources?.getStringArray(R.array.playbackSpeed)!!
+ val playbackSpeedValues =
+ context?.resources?.getStringArray(R.array.playbackSpeedValues)!!
+
+ // change playback speed dialog
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.change_playback_speed)
+ .setItems(playbackSpeeds) { _, index ->
+ // set the new playback speed
+ val newPlaybackSpeed = playbackSpeedValues[index].toFloat()
+ exoPlayer.setPlaybackSpeed(newPlaybackSpeed)
+ }
+ .show()
+ }
+
+ override fun onAspectRatioClicked() {
+ // switching between original aspect ratio (black bars) and zoomed to fill device screen
+ val aspectRatioModeNames = arrayOf(
+ context?.getString(R.string.resize_mode_fit),
+ context?.getString(R.string.resize_mode_zoom),
+ context?.getString(R.string.resize_mode_fill)
+ )
+
+ val aspectRatioModes = arrayOf(
+ AspectRatioFrameLayout.RESIZE_MODE_FIT,
+ AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
+ AspectRatioFrameLayout.RESIZE_MODE_FILL
+ )
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.aspect_ratio)
+ .setItems(aspectRatioModeNames) { _, index ->
+ exoPlayerView.resizeMode = aspectRatioModes[index]
+ }
+ .show()
+ }
+
+ override fun onRepeatModeClicked() {
+ val repeatModeNames = arrayOf(
+ context?.getString(R.string.repeat_mode_none),
+ context?.getString(R.string.repeat_mode_current)
+ )
+
+ val repeatModes = arrayOf(
+ RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL,
+ RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
+ )
+ // repeat mode options dialog
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.repeat_mode)
+ .setItems(repeatModeNames) { _, index ->
+ exoPlayer.repeatMode = repeatModes[index]
+ }
+ .show()
+ }
+ }
+
+ // actions that don't depend on video information
+ private fun initializeOnClickActions() {
binding.closeImageView.setOnClickListener {
- Globals.isMiniPlayerVisible = false
+ Globals.MINI_PLAYER_VISIBLE = false
binding.playerMotionLayout.transitionToEnd()
val mainActivity = activity as MainActivity
mainActivity.supportFragmentManager.beginTransaction()
@@ -269,22 +561,30 @@ class PlayerFragment : Fragment() {
.commit()
}
playerBinding.closeImageButton.setOnClickListener {
- Globals.isMiniPlayerVisible = false
+ Globals.MINI_PLAYER_VISIBLE = false
binding.playerMotionLayout.transitionToEnd()
val mainActivity = activity as MainActivity
mainActivity.supportFragmentManager.beginTransaction()
.remove(this)
.commit()
}
+ // show the advanced player options
+ playerBinding.toggleOptions.setOnClickListener {
+ val bottomSheetFragment = BottomSheetFragment().apply {
+ setOnClickListeners(playerOptionsInterface)
+ }
+ bottomSheetFragment.show(childFragmentManager, null)
+ }
+
binding.playImageView.setOnClickListener {
- paused = if (paused) {
+ if (!exoPlayer.isPlaying) {
+ // start or go on playing
binding.playImageView.setImageResource(R.drawable.ic_pause)
exoPlayer.play()
- false
} else {
+ // pause the video
binding.playImageView.setImageResource(R.drawable.ic_play)
exoPlayer.pause()
- true
}
}
@@ -303,7 +603,7 @@ class PlayerFragment : Fragment() {
playerBinding.fullscreen.setOnClickListener {
// hide player controller
exoPlayerView.hideController()
- if (!Globals.isFullScreen) {
+ if (!Globals.IS_FULL_SCREEN) {
// go to fullscreen mode
setFullscreen()
} else {
@@ -312,17 +612,6 @@ class PlayerFragment : Fragment() {
}
}
- // switching between original aspect ratio (black bars) and zoomed to fill device screen
- playerBinding.aspectRatioButton.setOnClickListener {
- if (isZoomed) {
- exoPlayerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
- isZoomed = false
- } else {
- exoPlayerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
- isZoomed = true
- }
- }
-
// lock and unlock the player
playerBinding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon
@@ -339,6 +628,27 @@ class PlayerFragment : Fragment() {
isPlayerLocked = !isPlayerLocked
}
+ // set default playback speed
+ exoPlayer.setPlaybackSpeed(playbackSpeed.toFloat())
+
+ // share button
+ binding.relPlayerShare.setOnClickListener {
+ val shareDialog = ShareDialog(videoId!!, false, exoPlayer.currentPosition)
+ shareDialog.show(childFragmentManager, ShareDialog::class.java.name)
+ }
+
+ binding.relPlayerBackground.setOnClickListener {
+ // pause the current player
+ exoPlayer.pause()
+
+ // start the background mode
+ BackgroundHelper.playOnBackground(
+ requireContext(),
+ videoId!!,
+ exoPlayer.currentPosition
+ )
+ }
+
binding.playerScrollView.viewTreeObserver
.addOnScrollChangedListener {
if (binding.playerScrollView.getChildAt(0).bottom
@@ -349,11 +659,11 @@ class PlayerFragment : Fragment() {
}
}
- binding.commentsRecView.layoutManager = LinearLayoutManager(view.context)
+ binding.commentsRecView.layoutManager = LinearLayoutManager(view?.context)
binding.commentsRecView.setItemViewCacheSize(20)
binding.relatedRecView.layoutManager =
- GridLayoutManager(view.context, resources.getInteger(R.integer.grid_items))
+ GridLayoutManager(view?.context, resources.getInteger(R.integer.grid_items))
}
private fun setFullscreen() {
@@ -368,11 +678,6 @@ class PlayerFragment : Fragment() {
playerBinding.exoTitle.visibility = View.VISIBLE
val mainActivity = activity as MainActivity
- val fullscreenOrientationPref = PreferenceHelper
- .getString(requireContext(), "fullscreen_orientation", "ratio")
-
- scaleControls(1.3F)
-
if (!autoRotationEnabled) {
// different orientations of the video are only available when auto rotation is disabled
val orientation = when (fullscreenOrientationPref) {
@@ -381,17 +686,17 @@ class PlayerFragment : Fragment() {
// 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
+ else ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
- "auto" -> ActivityInfo.SCREEN_ORIENTATION_USER
- "landscape" -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ "auto" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR
+ "landscape" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
- else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
mainActivity.requestedOrientation = orientation
}
- Globals.isFullScreen = true
+ Globals.IS_FULL_SCREEN = true
}
private fun unsetFullscreen() {
@@ -406,26 +711,34 @@ class PlayerFragment : Fragment() {
playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen)
playerBinding.exoTitle.visibility = View.INVISIBLE
- 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
+ Globals.IS_FULL_SCREEN = false
}
private fun toggleDescription() {
- binding.playerDescriptionArrow.animate().rotationBy(180F).setDuration(250).start()
- binding.descLinLayout.visibility =
- if (binding.descLinLayout.isVisible) View.GONE else View.VISIBLE
+ if (binding.descLinLayout.isVisible) {
+ // hide the description and chapters
+ binding.playerDescriptionArrow.animate().rotation(0F).setDuration(250).start()
+ binding.descLinLayout.visibility = View.GONE
+ } else {
+ // show the description and chapters
+ binding.playerDescriptionArrow.animate().rotation(180F).setDuration(250).start()
+ binding.descLinLayout.visibility = View.VISIBLE
+ }
+ if (this::chapters.isInitialized && chapters.isNotEmpty()) {
+ val chapterIndex = getCurrentChapterIndex()
+ // scroll to the current chapter in the chapterRecView in the description
+ val layoutManager = binding.chaptersRecView.layoutManager as LinearLayoutManager
+ layoutManager.scrollToPositionWithOffset(chapterIndex, 0)
+ // set selected
+ val chaptersAdapter = binding.chaptersRecView.adapter as ChaptersAdapter
+ chaptersAdapter.updateSelectedPosition(chapterIndex)
+ }
}
private fun toggleComments() {
@@ -437,12 +750,7 @@ class PlayerFragment : Fragment() {
}
override fun onPause() {
- // pause the player if the screen is turned off
- val pausePlayerOnScreenOffEnabled = PreferenceHelper.getBoolean(
- requireContext(),
- "pause_screen_off",
- false
- )
+ // pauses the player if the screen is turned off
// check whether the screen is on
val pm = context?.getSystemService(Context.POWER_SERVICE) as PowerManager
@@ -461,53 +769,42 @@ class PlayerFragment : Fragment() {
super.onDestroy()
try {
saveWatchPosition()
- mediaSession.isActive = false
- mediaSession.release()
- mediaSessionConnector.setPlayer(null)
- playerNotification.setPlayer(null)
- val notificationManager = context?.getSystemService(
- Context.NOTIFICATION_SERVICE
- ) as NotificationManager
- notificationManager.cancel(1)
- exoPlayer.release()
+ nowPlayingNotification.destroy()
+ activity?.requestedOrientation =
+ if ((activity as MainActivity).autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
+ else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
} catch (e: Exception) {
}
}
// save the watch position if video isn't finished and option enabled
private fun saveWatchPosition() {
- val watchPositionsEnabled = PreferenceHelper.getBoolean(
- requireContext(),
- "watch_positions_toggle",
- true
- )
if (watchPositionsEnabled && exoPlayer.currentPosition != exoPlayer.duration) {
PreferenceHelper.saveWatchPosition(
- requireContext(),
videoId!!,
exoPlayer.currentPosition
)
} else if (watchPositionsEnabled) {
// delete watch position if video has ended
- PreferenceHelper.removeWatchPosition(requireContext(), videoId!!)
+ PreferenceHelper.removeWatchPosition(videoId!!)
}
}
private fun checkForSegments() {
- if (!exoPlayer.isPlaying || !sponsorBlockPrefs.sponsorBlockEnabled) return
+ if (!exoPlayer.isPlaying || !sponsorBlockEnabled) return
- exoPlayerView.postDelayed(this::checkForSegments, 100)
+ Handler(Looper.getMainLooper()).postDelayed(this::checkForSegments, 100)
if (!::segmentData.isInitialized || segmentData.segments.isEmpty()) {
return
}
segmentData.segments.forEach { segment: Segment ->
- val segmentStart = (segment.segment!![0] * 1000.0f).toLong()
- val segmentEnd = (segment.segment[1] * 1000.0f).toLong()
+ val segmentStart = (segment.segment!![0] * 1000f).toLong()
+ val segmentEnd = (segment.segment[1] * 1000f).toLong()
val currentPosition = exoPlayer.currentPosition
if (currentPosition in segmentStart until segmentEnd) {
- if (sponsorBlockPrefs.sponsorNotificationsEnabled) {
+ if (sponsorBlockNotifications) {
Toast.makeText(context, R.string.segment_skipped, Toast.LENGTH_SHORT).show()
}
exoPlayer.seekTo(segmentEnd)
@@ -515,61 +812,101 @@ class PlayerFragment : Fragment() {
}
}
- private fun playVideo(view: View) {
- fun run() {
- lifecycleScope.launchWhenCreated {
- val response = try {
- RetrofitInstance.api.getStreams(videoId!!)
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection")
- Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response")
- Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
- return@launchWhenCreated
- }
- // for the notification description adapter
- title = response.title!!
- uploader = response.uploader!!
- thumbnailUrl = response.thumbnailUrl!!
+ private fun playVideo() {
+ Globals.playingQueue += videoId!!
+ lifecycleScope.launchWhenCreated {
+ streams = try {
+ RetrofitInstance.api.getStreams(videoId!!)
+ } catch (e: IOException) {
+ println(e)
+ Log.e(TAG, "IOException, you might not have internet connection")
+ Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
+ return@launchWhenCreated
+ } catch (e: HttpException) {
+ Log.e(TAG, "HttpException, unexpected response")
+ Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
+ return@launchWhenCreated
+ }
- // save related streams for autoplay
- relatedStreams = response.relatedStreams
+ runOnUiThread {
+ // set media sources for the player
+ setResolutionAndSubtitles()
+ prepareExoPlayerView()
+ initializePlayerView(streams)
+ if (!isLive) seekToWatchPosition()
+ exoPlayer.prepare()
+ exoPlayer.play()
+ // show controllers when not in picture in picture mode
+ if (!activity?.isInPictureInPictureMode!!) exoPlayerView.useController = true
+ initializePlayerNotification()
+ if (sponsorBlockEnabled) fetchSponsorBlockSegments()
+ // show comments if related streams disabled
+ if (!relatedStreamsEnabled) toggleComments()
+ // prepare for autoplay
+ if (autoplayEnabled) setNextStream()
+ if (watchHistoryEnabled) PreferenceHelper.addToWatchHistory(videoId!!, streams)
+ }
+ }
+ }
- runOnUiThread {
- // set media sources for the player
- setResolutionAndSubtitles(response)
- prepareExoPlayerView()
- initializePlayerView(view, response)
- seekToWatchPosition()
- exoPlayer.prepare()
- exoPlayer.play()
- exoPlayerView.useController = true
- initializePlayerNotification(requireContext())
- fetchSponsorBlockSegments()
- // show comments if related streams disabled
- if (!relatedStreamsEnabled) toggleComments()
- // prepare for autoplay
- initAutoPlay()
- val watchHistoryEnabled =
- PreferenceHelper.getBoolean(requireContext(), "Watch_history_toggle", true)
- if (watchHistoryEnabled) {
- PreferenceHelper.addToWatchHistory(requireContext(), videoId!!, response)
- }
+ /**
+ * set the videoId of the next stream for autoplay
+ */
+ private fun setNextStream() {
+ if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId)
+ // search for the next videoId in the playlist
+ lifecycleScope.launchWhenCreated {
+ nextStreamId = autoPlayHelper.getNextVideoId(videoId!!, streams.relatedStreams!!)
+ }
+ }
+
+ /**
+ * fetch the segments for SponsorBlock
+ */
+ private fun fetchSponsorBlockSegments() {
+ CoroutineScope(Dispatchers.IO).launch {
+ kotlin.runCatching {
+ val categories = PlayerHelper.getSponsorBlockCategories()
+ if (categories.size > 0) {
+ segmentData =
+ RetrofitInstance.api.getSegments(
+ videoId!!,
+ ObjectMapper().writeValueAsString(categories)
+ )
}
}
}
- run()
+ }
+
+ private fun refreshLiveStatus() {
+ // switch back to normal speed when on the end of live stream
+ if (exoPlayer.duration - exoPlayer.currentPosition < 7000) {
+ exoPlayer.setPlaybackSpeed(1F)
+ playerBinding.liveSeparator.visibility = View.GONE
+ playerBinding.liveDiff.text = ""
+ } else {
+ Log.e(TAG, "changing the time")
+ // live stream but not watching at the end/live position
+ playerBinding.liveSeparator.visibility = View.VISIBLE
+ val diffText = DateUtils.formatElapsedTime(
+ (exoPlayer.duration - exoPlayer.currentPosition) / 1000
+ )
+ playerBinding.liveDiff.text = "-$diffText"
+ }
+ // call it again
+ Handler(Looper.getMainLooper())
+ .postDelayed(this@PlayerFragment::refreshLiveStatus, 100)
}
private fun seekToWatchPosition() {
// seek to saved watch position if available
- val watchPositions = PreferenceHelper.getWatchPositions(requireContext())
+ val watchPositions = PreferenceHelper.getWatchPositions()
var position: Long? = null
watchPositions.forEach {
- if (it.videoId == videoId) position = it.position
+ if (it.videoId == videoId &&
+ // don't seek to the position if it's the end, autoplay would skip it immediately
+ streams.duration!! - it.position / 1000 > 2
+ ) position = it.position
}
// support for time stamped links
val timeStamp: Long? = arguments?.getLong("timeStamp")
@@ -579,141 +916,20 @@ class PlayerFragment : Fragment() {
if (position != null) exoPlayer.seekTo(position!!)
}
- // the function is working recursively
- private fun initAutoPlay() {
- // save related streams for autoplay
- if (autoplay) {
- // if it's a playlist use the next video
- if (playlistId != null) {
- lateinit var playlist: Playlist // var for saving the list in
- // runs only the first time when starting a video from a playlist
- if (playlistStreamIds.isEmpty()) {
- CoroutineScope(Dispatchers.IO).launch {
- // fetch the playlists videos
- playlist = RetrofitInstance.api.getPlaylist(playlistId!!)
- // save the playlist urls in the array
- playlist.relatedStreams?.forEach { video ->
- playlistStreamIds += video.url?.replace("/watch?v=", "")!!
- }
- // save playlistNextPage for usage if video is not contained
- playlistNextPage = playlist.nextpage
- // restart the function after videos are loaded
- initAutoPlay()
- }
- }
- // if the playlists contain the video, then save the next video as next stream
- else if (playlistStreamIds.contains(videoId)) {
- val index = playlistStreamIds.indexOf(videoId)
- // check whether there's a next video
- if (index + 1 <= playlistStreamIds.size) {
- nextStreamId = playlistStreamIds[index + 1]
- }
- // fetch the next page of the playlist if the video isn't contained
- } else if (playlistNextPage != null) {
- CoroutineScope(Dispatchers.IO).launch {
- RetrofitInstance.api.getPlaylistNextPage(playlistId!!, playlistNextPage!!)
- // append all the playlist item urls to the array
- playlist.relatedStreams?.forEach { video ->
- playlistStreamIds += video.url?.replace("/watch?v=", "")!!
- }
- // save playlistNextPage for usage if video is not contained
- playlistNextPage = playlist.nextpage
- // restart the function after videos are loaded
- initAutoPlay()
- }
- }
- // else: the video must be the last video of the playlist so nothing happens
-
- // if it's not a playlist then use the next related video
- } else if (relatedStreams != null && relatedStreams!!.isNotEmpty()) {
- // save next video from related streams for autoplay
- nextStreamId = relatedStreams!![0].url!!.replace("/watch?v=", "")
- }
- }
- }
-
// used for autoplay and skipping to next video
private fun playNextVideo() {
+ if (nextStreamId == null) return
// check whether there is a new video in the queue
+ val nextQueueVideo = autoPlayHelper.getNextPlayingQueueVideoId(videoId!!)
+ if (nextQueueVideo != null) nextStreamId = nextQueueVideo
// by making sure that the next and the current video aren't the same
- if (videoId != nextStreamId) {
- // save the id of the next stream as videoId and load the next video
- videoId = nextStreamId
- playVideo(view!!)
- }
- }
-
- private fun setSponsorBlockPrefs() {
- sponsorBlockPrefs.sponsorBlockEnabled =
- PreferenceHelper.getBoolean(requireContext(), "sb_enabled_key", true)
- sponsorBlockPrefs.sponsorNotificationsEnabled =
- PreferenceHelper.getBoolean(requireContext(), "sb_notifications_key", true)
- sponsorBlockPrefs.introEnabled =
- PreferenceHelper.getBoolean(requireContext(), "intro_category_key", false)
- sponsorBlockPrefs.selfPromoEnabled =
- PreferenceHelper.getBoolean(requireContext(), "selfpromo_category_key", false)
- sponsorBlockPrefs.interactionEnabled =
- PreferenceHelper.getBoolean(requireContext(), "interaction_category_key", false)
- sponsorBlockPrefs.sponsorsEnabled =
- PreferenceHelper.getBoolean(requireContext(), "sponsors_category_key", true)
- sponsorBlockPrefs.outroEnabled =
- PreferenceHelper.getBoolean(requireContext(), "outro_category_key", false)
- sponsorBlockPrefs.fillerEnabled =
- PreferenceHelper.getBoolean(requireContext(), "filler_category_key", false)
- sponsorBlockPrefs.musicOffTopicEnabled =
- PreferenceHelper.getBoolean(requireContext(), "music_offtopic_category_key", false)
- sponsorBlockPrefs.previewEnabled =
- PreferenceHelper.getBoolean(requireContext(), "preview_category_key", false)
- }
-
- private fun fetchSponsorBlockSegments() {
- fun run() {
- lifecycleScope.launch(Dispatchers.IO) {
- if (sponsorBlockPrefs.sponsorBlockEnabled) {
- val categories: ArrayList = arrayListOf()
- if (sponsorBlockPrefs.introEnabled) {
- categories.add("intro")
- }
- if (sponsorBlockPrefs.selfPromoEnabled) {
- categories.add("selfpromo")
- }
- if (sponsorBlockPrefs.interactionEnabled) {
- categories.add("interaction")
- }
- if (sponsorBlockPrefs.sponsorsEnabled) {
- categories.add("sponsor")
- }
- if (sponsorBlockPrefs.outroEnabled) {
- categories.add("outro")
- }
- if (sponsorBlockPrefs.fillerEnabled) {
- categories.add("filler")
- }
- if (sponsorBlockPrefs.musicOffTopicEnabled) {
- categories.add("music_offtopic")
- }
- if (sponsorBlockPrefs.previewEnabled) {
- categories.add("preview")
- }
- if (categories.size > 0) {
- segmentData = try {
- RetrofitInstance.api.getSegments(
- videoId!!,
- "[\"" + TextUtils.join("\",\"", categories) + "\"]"
- )
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection")
- return@launch
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response")
- return@launch
- }
- }
- }
- }
- }
- run()
+ saveWatchPosition()
+ // forces the comments to reload for the new video
+ commentsLoaded = false
+ binding.commentsRecView.adapter = null
+ // save the id of the next stream as videoId and load the next video
+ videoId = nextStreamId
+ playVideo()
}
private fun prepareExoPlayerView() {
@@ -726,24 +942,49 @@ class PlayerFragment : Fragment() {
useController = false
player = exoPlayer
}
+
+ if (useSystemCaptionStyle) {
+ // set the subtitle style
+ val captionStyle = PlayerHelper.getCaptionStyle(requireContext())
+ exoPlayerView.subtitleView?.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT)
+ exoPlayerView.subtitleView?.setStyle(captionStyle)
+ }
}
- private fun initializePlayerView(view: View, response: Streams) {
- binding.playerViewsInfo.text =
- context?.getString(R.string.views, response.views.formatShort()) +
- " • " + response.uploadDate
- binding.textLike.text = response.likes.formatShort()
- binding.textDislike.text = response.dislikes.formatShort()
- Picasso.get().load(response.uploaderAvatar).into(binding.playerChannelImage)
- binding.playerChannelName.text = response.uploader
+ private fun handleLiveVideo() {
+ playerBinding.exoTime.visibility = View.GONE
+ playerBinding.liveLL.visibility = View.VISIBLE
+ playerBinding.liveIndicator.setOnClickListener {
+ exoPlayer.seekTo(exoPlayer.duration - 1000)
+ }
+ refreshLiveStatus()
+ }
- binding.titleTextView.text = response.title
- binding.playerTitle.text = response.title
- binding.playerDescription.text = response.description
+ private fun initializePlayerView(response: Streams) {
+ binding.apply {
+ playerViewsInfo.text =
+ context?.getString(R.string.views, response.views.formatShort()) +
+ if (!isLive) " • " + response.uploadDate else ""
+
+ textLike.text = response.likes.formatShort()
+ textDislike.text = response.dislikes.formatShort()
+ ConnectionHelper.loadImage(response.uploaderAvatar, binding.playerChannelImage)
+ playerChannelName.text = response.uploader
+
+ titleTextView.text = response.title
+
+ playerTitle.text = response.title
+ playerDescription.text = response.description
+ }
+
+ // duration that's not greater than 0 indicates that the video is live
+ if (response.duration!! <= 0) {
+ isLive = true
+ handleLiveVideo()
+ }
playerBinding.exoTitle.text = response.title
- enableSeekbarPreview()
enableDoubleTapToSeek()
// init the chapters recyclerview
@@ -752,34 +993,11 @@ class PlayerFragment : Fragment() {
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 {
override fun onIsPlayingChanged(isPlaying: Boolean) {
- if (isPlaying && sponsorBlockPrefs.sponsorBlockEnabled) {
- exoPlayerView.postDelayed(
+ if (isPlaying && sponsorBlockEnabled) {
+ Handler(Looper.getMainLooper()).postDelayed(
this@PlayerFragment::checkForSegments,
100
)
@@ -818,11 +1036,11 @@ class PlayerFragment : Fragment() {
playbackState == Player.STATE_ENDED &&
nextStreamId != null &&
!transitioning &&
- autoplay
+ autoplayEnabled
) {
transitioning = true
// check whether autoplay is enabled
- if (autoplay) playNextVideo()
+ if (autoplayEnabled) playNextVideo()
}
if (playWhenReady && playbackState == Player.STATE_READY) {
@@ -841,47 +1059,16 @@ 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) {
+ if (response.duration > 0) {
// download clicked
binding.relPlayerDownload.setOnClickListener {
- if (!IS_DOWNLOAD_RUNNING) {
+ if (!Globals.IS_DOWNLOAD_RUNNING) {
val newFragment = DownloadDialog()
val bundle = Bundle()
bundle.putString("video_id", videoId)
newFragment.arguments = bundle
- newFragment.show(childFragmentManager, "DownloadDialog")
+ newFragment.show(childFragmentManager, DownloadDialog::class.java.name)
} else {
Toast.makeText(context, R.string.dlisinprogress, Toast.LENGTH_SHORT)
.show()
@@ -892,19 +1079,23 @@ class PlayerFragment : Fragment() {
}
if (response.hls != null) {
- binding.relPlayerVlc.setOnClickListener {
+ binding.relPlayerOpen.setOnClickListener {
// start an intent with video as mimetype using the hls stream
val uri: Uri = Uri.parse(response.hls)
val intent = Intent()
intent.action = Intent.ACTION_VIEW
intent.setDataAndType(uri, "video/*")
- intent.putExtra(Intent.EXTRA_TITLE, title)
- intent.putExtra("title", title)
- intent.putExtra("artist", uploader)
+ intent.putExtra(Intent.EXTRA_TITLE, streams.title)
+ intent.putExtra("title", streams.title)
+ intent.putExtra("artist", streams.uploader)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
+ try {
+ startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(context, R.string.no_player_found, Toast.LENGTH_SHORT).show()
+ }
}
}
if (relatedStreamsEnabled) {
@@ -930,104 +1121,119 @@ class PlayerFragment : Fragment() {
}
binding.playerChannel.setOnClickListener {
- val activity = view.context as MainActivity
+ val activity = view?.context as MainActivity
val bundle = bundleOf("channel_id" to response.uploaderUrl)
activity.navController.navigate(R.id.channelFragment, bundle)
activity.binding.mainMotionLayout.transitionToEnd()
binding.playerMotionLayout.transitionToEnd()
}
- val token = PreferenceHelper.getToken(requireContext())
if (token != "") {
- val channelId = response.uploaderUrl?.replace("/channel/", "")
- isSubscribed(binding.playerSubscribe, channelId!!)
+ isSubscribed()
binding.relPlayerSave.setOnClickListener {
- val newFragment = AddtoPlaylistDialog()
+ val newFragment = AddToPlaylistDialog()
val bundle = Bundle()
bundle.putString("videoId", videoId)
newFragment.arguments = bundle
- newFragment.show(childFragmentManager, "AddToPlaylist")
+ newFragment.show(childFragmentManager, AddToPlaylistDialog::class.java.name)
}
+ } else {
+ binding.relPlayerSave.setOnClickListener {
+ Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // next and previous buttons
+ playerBinding.skipPrev.visibility = if (
+ skipButtonsEnabled && Globals.playingQueue.indexOf(videoId!!) != 0
+ ) View.VISIBLE else View.INVISIBLE
+ playerBinding.skipNext.visibility = if (skipButtonsEnabled) View.VISIBLE else View.INVISIBLE
+
+ playerBinding.skipPrev.setOnClickListener {
+ val index = Globals.playingQueue.indexOf(videoId!!) - 1
+ videoId = Globals.playingQueue[index]
+ playVideo()
+ }
+
+ playerBinding.skipNext.setOnClickListener {
+ playNextVideo()
}
}
private fun enableDoubleTapToSeek() {
- val seekIncrement =
- PreferenceHelper.getString(requireContext(), "seek_increment", "5")?.toLong()!! * 1000
-
- // enable rewind button
- binding.rewindFL.setOnClickListener(
- DoubleClickListener(
- callback = object : DoubleClickListener.Callback {
- override fun doubleClicked() {
- binding.rewindBTN.visibility = View.VISIBLE
- exoPlayer.seekTo(exoPlayer.currentPosition - seekIncrement)
- Handler(Looper.getMainLooper()).postDelayed({
- binding.rewindBTN.visibility = View.INVISIBLE
- }, 700)
- }
-
- override fun singleClicked() {
- toggleController()
+ // set seek increment text
+ val seekIncrementText = (seekIncrement / 1000).toString()
+ doubleTapOverlayBinding.rewindTV.text = seekIncrementText
+ doubleTapOverlayBinding.forwardTV.text = seekIncrementText
+ binding.player.setOnDoubleTapListener(
+ object : DoubleTapInterface {
+ override fun onEvent(x: Float) {
+ val width = exoPlayerView.width
+ when {
+ width * 0.5 > x -> rewind()
+ width * 0.5 < x -> forward()
}
}
- )
- )
-
- // enable fast forward button
- binding.forwardFL.setOnClickListener(
- DoubleClickListener(
- callback = object : DoubleClickListener.Callback {
- override fun doubleClicked() {
- binding.forwardBTN.visibility = View.VISIBLE
- exoPlayer.seekTo(exoPlayer.currentPosition + seekIncrement)
- Handler(Looper.getMainLooper()).postDelayed({
- binding.forwardBTN.visibility = View.INVISIBLE
- }, 700)
- }
-
- override fun singleClicked() {
- toggleController()
- }
- }
- )
+ }
)
}
- private fun disableDoubleTapToSeek() {
- // disable fast forward and rewind by double tapping
- binding.forwardFL.visibility = View.GONE
- binding.rewindFL.visibility = View.GONE
+ private fun rewind() {
+ exoPlayer.seekTo(exoPlayer.currentPosition - seekIncrement)
+
+ // show the rewind button
+ doubleTapOverlayBinding.rewindBTN.apply {
+ visibility = View.VISIBLE
+ // clear previous animation
+ animate().rotation(0F).setDuration(0).start()
+ // start new animation
+ animate()
+ .rotation(-30F)
+ .setDuration(100)
+ .withEndAction {
+ // reset the animation when finished
+ animate().rotation(0F).setDuration(100).start()
+ }
+ .start()
+
+ removeCallbacks(hideRewindButtonRunnable)
+ // start callback to hide the button
+ postDelayed(hideRewindButtonRunnable, 700)
+ }
}
- // toggle the visibility of the player controller
- private fun toggleController() {
- if (exoPlayerView.isControllerFullyVisible) exoPlayerView.hideController()
- else exoPlayerView.showController()
+ private fun forward() {
+ exoPlayer.seekTo(exoPlayer.currentPosition + seekIncrement)
+
+ // show the forward button
+ doubleTapOverlayBinding.forwardBTN.apply {
+ visibility = View.VISIBLE
+ // clear previous animation
+ animate().rotation(0F).setDuration(0).start()
+ // start new animation
+ animate()
+ .rotation(30F)
+ .setDuration(100)
+ .withEndAction {
+ // reset the animation when finished
+ animate().rotation(0F).setDuration(100).start()
+ }
+ .start()
+
+ // start callback to hide the button
+ removeCallbacks(hideForwardButtonRunnable)
+ postDelayed(hideForwardButtonRunnable, 700)
+ }
}
- // enable seek bar preview
- private fun enableSeekbarPreview() {
- playerBinding.exoProgress.addListener(object : TimeBar.OnScrubListener {
- override fun onScrubStart(timeBar: TimeBar, position: Long) {
- exoPlayer.pause()
- }
-
- override fun onScrubMove(timeBar: TimeBar, position: Long) {
- 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()
- }, 200)
- }
- })
+ private val hideForwardButtonRunnable = Runnable {
+ doubleTapOverlayBinding.forwardBTN.apply {
+ visibility = View.GONE
+ }
+ }
+ private val hideRewindButtonRunnable = Runnable {
+ doubleTapOverlayBinding.rewindBTN.apply {
+ visibility = View.GONE
+ }
}
private fun initializeChapters() {
@@ -1049,7 +1255,7 @@ class PlayerFragment : Fragment() {
}
playerBinding.chapterLL.visibility = View.VISIBLE
playerBinding.chapterLL.setOnClickListener {
- if (Globals.isFullScreen) {
+ if (Globals.IS_FULL_SCREEN) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chapters)
.setItems(titles.toTypedArray()) { _, index ->
@@ -1070,27 +1276,31 @@ class PlayerFragment : Fragment() {
// call the function again in 100ms
exoPlayerView.postDelayed(this::setCurrentChapterName, 100)
- val chapterName = getCurrentChapterName()
+ val chapterIndex = getCurrentChapterIndex()
+ val chapterName = chapters[chapterIndex].title
// change the chapter name textView text to the chapterName
- if (chapterName != null && chapterName != playerBinding.chapterName.text) {
+ if (chapterName != playerBinding.chapterName.text) {
playerBinding.chapterName.text = chapterName
+ // update the selected item
+ val chaptersAdapter = binding.chaptersRecView.adapter as ChaptersAdapter
+ chaptersAdapter.updateSelectedPosition(chapterIndex)
}
}
// get the name of the currently played chapter
- private fun getCurrentChapterName(): String? {
+ private fun getCurrentChapterIndex(): Int {
val currentPosition = exoPlayer.currentPosition
- var chapterName: String? = null
+ var chapterIndex = 0
- chapters.forEach {
+ chapters.forEachIndexed { index, chapter ->
// check whether the chapter start is greater than the current player position
- if (currentPosition >= it.start!! * 1000) {
+ if (currentPosition >= chapter.start!! * 1000) {
// save chapter title if found
- chapterName = it.title
+ chapterIndex = index
}
}
- return chapterName
+ return chapterIndex
}
private fun setMediaSource(
@@ -1114,22 +1324,22 @@ class PlayerFragment : Fragment() {
exoPlayer.setMediaSource(mergeSource)
}
- private fun setResolutionAndSubtitles(response: Streams) {
- val videoFormatPreference =
- PreferenceHelper.getString(requireContext(), "player_video_format", "WEBM")
+ private fun getAvailableResolutions(): Pair, Array> {
+ if (!this::streams.isInitialized) return Pair(arrayOf(), arrayOf())
- var videosNameArray: Array = arrayOf()
+ var videosNameArray: Array = arrayOf()
var videosUrlArray: Array = arrayOf()
// append hls to list if available
- if (response.hls != null) {
+ if (streams.hls != null) {
videosNameArray += getString(R.string.hls)
- videosUrlArray += response.hls.toUri()
+ videosUrlArray += streams.hls!!.toUri()
}
- for (vid in response.videoStreams!!) {
+ for (vid in streams.videoStreams!!) {
// append quality to list if it has the preferred format (e.g. MPEG)
- if (vid.format.equals(videoFormatPreference) && vid.url != null) { // preferred format
+ val preferredMimeType = "video/$videoFormatPreference"
+ if (vid.url != null && vid.mimeType == preferredMimeType) { // preferred format
videosNameArray += vid.quality.toString()
videosUrlArray += vid.url!!.toUri()
} else if (vid.quality.equals("LBRY") && vid.format.equals("MP4")) { // LBRY MP4 format
@@ -1137,76 +1347,56 @@ class PlayerFragment : Fragment() {
videosUrlArray += vid.url!!.toUri()
}
}
+ return Pair(videosNameArray, videosUrlArray)
+ }
+
+ private fun setResolutionAndSubtitles() {
+ // get the available resolutions
+ val (videosNameArray, videosUrlArray) = getAvailableResolutions()
+
// create a list of subtitles
- subtitle = mutableListOf()
- response.subtitles!!.forEach {
+ subtitle = mutableListOf()
+ val subtitlesNamesList = mutableListOf(context?.getString(R.string.none)!!)
+ val subtitleCodesList = mutableListOf("")
+ streams.subtitles!!.forEach {
subtitle.add(
SubtitleConfiguration.Builder(it.url!!.toUri())
.setMimeType(it.mimeType!!) // The correct MIME type (required).
.setLanguage(it.code) // The subtitle language (optional).
.build()
)
+ subtitlesNamesList += it.name!!
+ subtitleCodesList += it.code!!
}
+
+ // set the default subtitle if available
+ if (defaultSubtitleCode != "" && subtitleCodesList.contains(defaultSubtitleCode)) {
+ val newParams = trackSelector.buildUponParameters()
+ .setPreferredTextLanguage(defaultSubtitleCode)
+ .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
+ trackSelector.setParameters(newParams)
+ }
+
// set media source and resolution in the beginning
setStreamSource(
- response,
+ streams,
videosNameArray,
videosUrlArray
)
-
- playerBinding.qualityText.setOnClickListener {
- // Dialog for quality selection
- val builder: MaterialAlertDialogBuilder? = activity?.let {
- MaterialAlertDialogBuilder(it)
- }
- val lastPosition = exoPlayer.currentPosition
- builder!!.setTitle(R.string.choose_quality_dialog)
- .setItems(
- videosNameArray
- ) { _, which ->
- whichQuality = which
- if (
- videosNameArray[which] == getString(R.string.hls) ||
- videosNameArray[which] == "LBRY HLS"
- ) {
- // no need to merge sources if using hls
- val mediaItem: MediaItem = MediaItem.Builder()
- .setUri(videosUrlArray[which])
- .setSubtitleConfigurations(subtitle)
- .build()
- exoPlayer.setMediaItem(mediaItem)
- } else {
- val videoUri = videosUrlArray[which]
- val audioUrl = getMostBitRate(response.audioStreams!!)
- setMediaSource(videoUri, audioUrl)
- }
- exoPlayer.seekTo(lastPosition)
- playerBinding.qualityText.text = videosNameArray[which]
- }
- val dialog = builder.create()
- dialog.show()
- }
}
private fun setStreamSource(
streams: Streams,
- videosNameArray: Array,
+ videosNameArray: Array,
videosUrlArray: Array
) {
- val defRes = PreferenceHelper.getString(
- requireContext(),
- "default_resolution",
- "hls"
- )!!
-
- if (defRes != "hls") {
+ if (defRes != "") {
videosNameArray.forEachIndexed { index, pipedStream ->
// search for quality preference in the available stream sources
if (pipedStream.contains(defRes)) {
val videoUri = videosUrlArray[index]
- val audioUrl = getMostBitRate(streams.audioStreams!!)
+ val audioUrl = PlayerHelper.getAudioSource(streams.audioStreams!!)
setMediaSource(videoUri, audioUrl)
- playerBinding.qualityText.text = videosNameArray[index]
return
}
}
@@ -1219,23 +1409,18 @@ class PlayerFragment : Fragment() {
.setSubtitleConfigurations(subtitle)
.build()
exoPlayer.setMediaItem(mediaItem)
- playerBinding.qualityText.text = context?.getString(R.string.hls)
return
}
// if nothing found, use the first list entry
if (videosUrlArray.isNotEmpty()) {
val videoUri = videosUrlArray[0]
- val audioUrl = getMostBitRate(streams.audioStreams!!)
+ val audioUrl = PlayerHelper.getAudioSource(streams.audioStreams!!)
setMediaSource(videoUri, audioUrl)
- playerBinding.qualityText.text = videosNameArray[0]
}
}
- private fun createExoPlayer(view: View) {
- val bufferingGoal =
- PreferenceHelper.getString(requireContext(), "buffering_goal", "50")?.toInt()!! * 1000
-
+ private fun createExoPlayer() {
val cronetEngine: CronetEngine = CronetHelper.getCronetEngine()
val cronetDataSourceFactory: CronetDataSource.Factory =
CronetDataSource.Factory(cronetEngine, Executors.newCachedThreadPool())
@@ -1263,86 +1448,73 @@ class PlayerFragment : Fragment() {
)
.build()
- exoPlayer = ExoPlayer.Builder(view.context)
+ // control for the track sources like subtitles and audio source
+ trackSelector = DefaultTrackSelector(requireContext())
+
+ exoPlayer = ExoPlayer.Builder(requireContext())
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.setLoadControl(loadControl)
+ .setTrackSelector(trackSelector)
.build()
exoPlayer.setAudioAttributes(audioAttributes, true)
}
- private fun initializePlayerNotification(c: Context) {
- mediaSession = MediaSessionCompat(c, this.javaClass.name)
- mediaSession.apply {
- isActive = true
- }
-
- mediaSessionConnector = MediaSessionConnector(mediaSession)
- mediaSessionConnector.setPlayer(exoPlayer)
-
- playerNotification = PlayerNotificationManager
- .Builder(c, 1, "background_mode")
- .setMediaDescriptionAdapter(
- DescriptionAdapter(title, uploader, thumbnailUrl, requireContext())
- )
- .build()
-
- playerNotification.apply {
- setPlayer(exoPlayer)
- setUsePreviousAction(false)
- setUseStopAction(true)
- setMediaSessionToken(mediaSession.sessionToken)
+ /**
+ * show the [NowPlayingNotification] for the current video
+ */
+ private fun initializePlayerNotification() {
+ if (!this::nowPlayingNotification.isInitialized) {
+ nowPlayingNotification = NowPlayingNotification(requireContext(), exoPlayer)
}
+ nowPlayingNotification.updatePlayerNotification(streams)
}
// lock the player
private fun lockPlayer(isLocked: Boolean) {
+ // isLocked is the current (old) state of the player lock
val visibility = if (isLocked) View.VISIBLE else View.GONE
playerBinding.exoTopBarRight.visibility = visibility
- playerBinding.exoPlayPause.visibility = visibility
+ playerBinding.exoCenterControls.visibility = visibility
playerBinding.exoBottomBar.visibility = visibility
playerBinding.closeImageButton.visibility = visibility
playerBinding.exoTitle.visibility =
- if (isLocked && Globals.isFullScreen) View.VISIBLE else View.INVISIBLE
+ if (isLocked &&
+ Globals.IS_FULL_SCREEN
+ ) View.VISIBLE else View.INVISIBLE
// disable double tap to seek when the player is locked
- if (isLocked) enableDoubleTapToSeek() else disableDoubleTapToSeek()
+ if (isLocked) {
+ // enable fast forward and rewind by double tapping
+ enableDoubleTapToSeek()
+ } else {
+ // disable fast forward and rewind by double tapping
+ binding.player.setOnDoubleTapListener(null)
+ }
}
- private fun isSubscribed(button: MaterialButton, channel_id: String) {
- @SuppressLint("ResourceAsColor")
+ private fun isSubscribed() {
fun run() {
+ val channelId = streams.uploaderUrl.toID()
lifecycleScope.launchWhenCreated {
- val response = try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.isSubscribed(
- channel_id,
- token
- )
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection")
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response")
- return@launchWhenCreated
- }
+ isSubscribed = SubscriptionHelper.isSubscribed(channelId)
+
+ if (isSubscribed == null) return@launchWhenCreated
runOnUiThread {
- if (response.subscribed == true) {
- isSubscribed = true
- button.text = getString(R.string.unsubscribe)
+ if (isSubscribed == true) {
+ binding.playerSubscribe.text = getString(R.string.unsubscribe)
}
- if (response.subscribed != null) {
- button.setOnClickListener {
- if (isSubscribed) {
- unsubscribe(channel_id)
- button.text = getString(R.string.subscribe)
- } else {
- subscribe(channel_id)
- button.text = getString(R.string.unsubscribe)
- }
+ binding.playerSubscribe.setOnClickListener {
+ if (isSubscribed == true) {
+ SubscriptionHelper.unsubscribe(channelId)
+ binding.playerSubscribe.text = getString(R.string.subscribe)
+ isSubscribed = false
+ } else {
+ SubscriptionHelper.subscribe(channelId)
+ binding.playerSubscribe.text = getString(R.string.unsubscribe)
+ isSubscribed = true
}
}
}
@@ -1351,71 +1523,6 @@ class PlayerFragment : Fragment() {
run()
}
- private fun subscribe(channel_id: String) {
- fun run() {
- lifecycleScope.launchWhenCreated {
- try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.subscribe(
- token,
- Subscribe(channel_id)
- )
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection")
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response$e")
- return@launchWhenCreated
- }
- isSubscribed = true
- }
- }
- run()
- }
-
- private fun unsubscribe(channel_id: String) {
- fun run() {
- lifecycleScope.launchWhenCreated {
- try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.unsubscribe(
- token,
- Subscribe(channel_id)
- )
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection")
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response")
- return@launchWhenCreated
- }
- isSubscribed = false
- }
- }
- run()
- }
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
-
- private fun getMostBitRate(audios: List): String {
- var bitrate = 0
- var index = 0
- for ((i, audio) in audios.withIndex()) {
- val q = audio.quality!!.replace(" kbps", "").toInt()
- if (q > bitrate) {
- bitrate = q
- index = i
- }
- }
- return audios[index].url!!
- }
-
private fun fetchComments() {
lifecycleScope.launchWhenCreated {
val commentsResponse = try {
@@ -1461,31 +1568,57 @@ class PlayerFragment : Fragment() {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (isInPictureInPictureMode) {
+ // set portrait mode
+ unsetFullscreen()
+
// hide and disable exoPlayer controls
exoPlayerView.hideController()
exoPlayerView.useController = false
- unsetFullscreen()
+ with(binding.playerMotionLayout) {
+ getConstraintSet(R.id.start).constrainHeight(R.id.player, -1)
+ enableTransition(R.id.yt_transition, false)
+ }
+ binding.linLayout.visibility = View.GONE
- Globals.isFullScreen = false
+ Globals.IS_FULL_SCREEN = false
} else {
// enable exoPlayer controls again
exoPlayerView.useController = true
- // switch back to portrait mode
- unsetFullscreen()
+ with(binding.playerMotionLayout) {
+ getConstraintSet(R.id.start).constrainHeight(R.id.player, 0)
+ enableTransition(R.id.yt_transition, true)
+ }
+ binding.linLayout.visibility = View.VISIBLE
}
}
fun onUserLeaveHint() {
+ if (SDK_INT >= Build.VERSION_CODES.O && shouldStartPiP()) {
+ activity?.enterPictureInPictureMode(updatePipParams())
+ }
+ }
+
+ private fun shouldStartPiP(): Boolean {
val bounds = Rect()
binding.playerScrollView.getHitRect(bounds)
- if (SDK_INT >= Build.VERSION_CODES.O &&
- exoPlayer.isPlaying && (binding.playerScrollView.getLocalVisibleRect(bounds) || Globals.isFullScreen)
- ) {
- activity?.enterPictureInPictureMode(updatePipParams())
+ val backgroundModeRunning = isServiceRunning(requireContext(), BackgroundMode::class.java)
+
+ return (binding.playerScrollView.getLocalVisibleRect(bounds) || Globals.IS_FULL_SCREEN) &&
+ (exoPlayer.isPlaying || !backgroundModeRunning)
+ }
+
+ private fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
+ val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ @Suppress("DEPRECATION")
+ for (service in manager.getRunningServices(Int.MAX_VALUE)) {
+ if (serviceClass.name == service.service.className) {
+ return true
+ }
}
+ return false
}
private fun updatePipParams() = PictureInPictureParams.Builder()
diff --git a/app/src/main/java/com/github/libretube/fragments/PlaylistFragment.kt b/app/src/main/java/com/github/libretube/fragments/PlaylistFragment.kt
index 57ac348a5..0aa9a15e4 100644
--- a/app/src/main/java/com/github/libretube/fragments/PlaylistFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/PlaylistFragment.kt
@@ -5,24 +5,27 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.adapters.PlaylistAdapter
import com.github.libretube.databinding.FragmentPlaylistBinding
import com.github.libretube.dialogs.PlaylistOptionsDialog
-import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.extensions.BaseFragment
import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.toID
import retrofit2.HttpException
import java.io.IOException
-class PlaylistFragment : Fragment() {
+class PlaylistFragment : BaseFragment() {
private val TAG = "PlaylistFragment"
private lateinit var binding: FragmentPlaylistBinding
private var playlistId: String? = null
- var nextPage: String? = null
+ private var isOwner: Boolean = false
+ private var nextPage: String? = null
private var playlistAdapter: PlaylistAdapter? = null
private var isLoading = true
@@ -30,6 +33,7 @@ class PlaylistFragment : Fragment() {
super.onCreate(savedInstanceState)
arguments?.let {
playlistId = it.getString("playlist_id")
+ isOwner = it.getBoolean("isOwner")
}
}
@@ -45,7 +49,7 @@ class PlaylistFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- playlistId = playlistId!!.replace("/playlist?list=", "")
+ playlistId = playlistId!!.toID()
binding.playlistRecView.layoutManager = LinearLayoutManager(context)
binding.playlistProgress.visibility = View.VISIBLE
@@ -56,7 +60,9 @@ class PlaylistFragment : Fragment() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
- RetrofitInstance.api.getPlaylist(playlistId!!)
+ // load locally stored playlists with the auth api
+ if (isOwner) RetrofitInstance.authApi.getPlaylist(playlistId!!)
+ else RetrofitInstance.api.getPlaylist(playlistId!!)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
@@ -70,20 +76,18 @@ class PlaylistFragment : Fragment() {
runOnUiThread {
binding.playlistProgress.visibility = View.GONE
binding.playlistName.text = response.name
- binding.playlistUploader.text = response.uploader
- binding.playlistTotVideos.text =
+ binding.uploader.text = response.uploader
+ binding.videoCount.text =
getString(R.string.videoCount, response.videos.toString())
- val user = PreferenceHelper.getUsername(requireContext())
- // check whether the user owns the playlist
- val isOwner = response.uploaderUrl == null &&
- response.uploader.equals(user, true)
-
// show playlist options
binding.optionsMenu.setOnClickListener {
val optionsDialog =
- PlaylistOptionsDialog(playlistId!!, isOwner, requireContext())
- optionsDialog.show(childFragmentManager, "PlaylistOptionsDialog")
+ PlaylistOptionsDialog(playlistId!!, isOwner)
+ optionsDialog.show(
+ childFragmentManager,
+ PlaylistOptionsDialog::class.java.name
+ )
}
playlistAdapter = PlaylistAdapter(
@@ -93,6 +97,16 @@ class PlaylistFragment : Fragment() {
requireActivity(),
childFragmentManager
)
+
+ // listen for playlist items to become deleted
+ playlistAdapter!!.registerAdapterDataObserver(object :
+ RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ binding.videoCount.text =
+ getString(R.string.videoCount, playlistAdapter!!.itemCount.toString())
+ }
+ })
+
binding.playlistRecView.adapter = playlistAdapter
binding.playlistScrollview.viewTreeObserver
.addOnScrollChangedListener {
@@ -104,10 +118,37 @@ class PlaylistFragment : Fragment() {
isLoading = true
fetchNextPage()
}
- } else {
- // scroll view is not at bottom
}
}
+
+ /**
+ * listener for swiping to the left or right
+ */
+ if (isOwner) {
+ val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
+ 0,
+ ItemTouchHelper.LEFT
+ ) {
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ return false
+ }
+
+ override fun onSwiped(
+ viewHolder: RecyclerView.ViewHolder,
+ direction: Int
+ ) {
+ val position = viewHolder.absoluteAdapterPosition
+ playlistAdapter!!.removeFromPlaylist(position)
+ }
+ }
+
+ val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
+ itemTouchHelper.attachToRecyclerView(binding.playlistRecView)
+ }
}
}
}
@@ -118,7 +159,14 @@ class PlaylistFragment : Fragment() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
- RetrofitInstance.api.getPlaylistNextPage(playlistId!!, nextPage!!)
+ // load locally stored playlists with the auth api
+ if (isOwner) RetrofitInstance.authApi.getPlaylistNextPage(
+ playlistId!!,
+ nextPage!!
+ ) else RetrofitInstance.api.getPlaylistNextPage(
+ playlistId!!,
+ nextPage!!
+ )
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
@@ -134,10 +182,4 @@ class PlaylistFragment : Fragment() {
}
run()
}
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
}
diff --git a/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt b/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt
index 1780eca4c..ca133d4e2 100644
--- a/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt
@@ -1,52 +1,34 @@
package com.github.libretube.fragments
-import android.content.Context
import android.os.Bundle
-import android.text.Editable
-import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
-import android.view.inputmethod.EditorInfo
-import android.view.inputmethod.InputMethodManager
-import android.widget.EditText
-import android.widget.TextView.GONE
-import android.widget.TextView.OnEditorActionListener
-import android.widget.TextView.VISIBLE
-import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
-import com.github.libretube.R
-import com.github.libretube.activities.hideKeyboard
-import com.github.libretube.adapters.SearchAdapter
+import com.github.libretube.activities.MainActivity
import com.github.libretube.adapters.SearchHistoryAdapter
import com.github.libretube.adapters.SearchSuggestionsAdapter
import com.github.libretube.databinding.FragmentSearchBinding
+import com.github.libretube.extensions.BaseFragment
+import com.github.libretube.models.SearchViewModel
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.RetrofitInstance
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
-class SearchFragment : Fragment() {
+class SearchFragment() : BaseFragment() {
private val TAG = "SearchFragment"
private lateinit var binding: FragmentSearchBinding
+ private val viewModel: SearchViewModel by activityViewModels()
- private var selectedFilter = 0
- private var apiSearchFilter = "all"
- private var nextPage: String? = null
-
- private var searchAdapter: SearchAdapter? = null
- private var isLoading: Boolean = true
- private var isFetchingSearch: Boolean = false
+ private var query: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- arguments?.let {
- }
+ query = arguments?.getString("query")
}
override fun onCreateView(
@@ -61,109 +43,25 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- var tempSelectedItem = 0
+ binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext())
- binding.clearSearchImageView.setOnClickListener {
- binding.autoCompleteTextView.text.clear()
+ // waiting for the query to change
+ viewModel.searchQuery.observe(viewLifecycleOwner) {
+ showData(it)
}
-
- binding.filterMenuImageView.setOnClickListener {
- val filterOptions = arrayOf(
- getString(R.string.all),
- getString(R.string.videos),
- getString(R.string.channels),
- getString(R.string.playlists),
- getString(R.string.music_songs),
- getString(R.string.music_videos),
- getString(R.string.music_albums),
- getString(R.string.music_playlists)
- )
-
- MaterialAlertDialogBuilder(view.context)
- .setTitle(getString(R.string.choose_filter))
- .setSingleChoiceItems(filterOptions, selectedFilter) { _, id ->
- tempSelectedItem = id
- }
- .setPositiveButton(
- getString(R.string.okay)
- ) { _, _ ->
- selectedFilter = tempSelectedItem
- apiSearchFilter = when (selectedFilter) {
- 0 -> "all"
- 1 -> "videos"
- 2 -> "channels"
- 3 -> "playlists"
- 4 -> "music_songs"
- 5 -> "music_videos"
- 6 -> "music_albums"
- 7 -> "music_playlists"
- else -> "all"
- }
- fetchSearch(binding.autoCompleteTextView.text.toString())
- }
- .setNegativeButton(getString(R.string.cancel), null)
- .create()
- .show()
- }
-
- // show search history
- binding.historyRecycler.layoutManager = LinearLayoutManager(view.context)
- showHistory()
-
- binding.searchRecycler.layoutManager = GridLayoutManager(view.context, 1)
- binding.autoCompleteTextView.requestFocus()
- val imm =
- requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.showSoftInput(binding.autoCompleteTextView, InputMethodManager.SHOW_IMPLICIT)
-
- binding.autoCompleteTextView.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(
- s: CharSequence?,
- start: Int,
- count: Int,
- after: Int
- ) {
- }
-
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
- if (s!! != "") {
- binding.searchRecycler.adapter = null
-
- binding.searchRecycler.viewTreeObserver
- .addOnScrollChangedListener {
- if (!binding.searchRecycler.canScrollVertically(1)) {
- fetchNextSearchItems(binding.autoCompleteTextView.text.toString())
- }
- }
- fetchSuggestions(s.toString(), binding.autoCompleteTextView)
- }
- }
-
- override fun afterTextChanged(s: Editable?) {
- if (s!!.isEmpty()) {
- showHistory()
- }
- }
- })
- binding.autoCompleteTextView.setOnEditorActionListener(
- OnEditorActionListener { _, actionId, _ ->
- if (actionId == EditorInfo.IME_ACTION_SEARCH) {
- hideKeyboard()
- binding.searchRecycler.visibility = VISIBLE
- binding.historyRecycler.visibility = GONE
- fetchSearch(binding.autoCompleteTextView.text.toString())
- return@OnEditorActionListener true
- }
- false
- }
- )
}
- private fun fetchSuggestions(query: String, autoTextView: EditText) {
+ private fun showData(query: String?) {
+ // fetch the search or history
+ binding.historyEmpty.visibility = View.GONE
+ binding.suggestionsRecycler.visibility = View.VISIBLE
+ if (query == null || query == "") showHistory()
+ else fetchSuggestions(query)
+ }
+
+ private fun fetchSuggestions(query: String) {
fun run() {
lifecycleScope.launchWhenCreated {
- binding.searchRecycler.visibility = GONE
- binding.historyRecycler.visibility = VISIBLE
val response = try {
RetrofitInstance.api.getSuggestions(query)
} catch (e: IOException) {
@@ -174,118 +72,33 @@ class SearchFragment : Fragment() {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
+ // only load the suggestions if the input field didn't get cleared yet
val suggestionsAdapter =
- SearchSuggestionsAdapter(response, autoTextView, this@SearchFragment)
- binding.historyRecycler.adapter = suggestionsAdapter
- }
- }
- if (!isFetchingSearch) run()
- }
-
- fun fetchSearch(query: String) {
- runOnUiThread {
- binding.historyRecycler.visibility = GONE
- }
- lifecycleScope.launchWhenCreated {
- isFetchingSearch = true
- hideKeyboard()
- val response = try {
- RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
- } catch (e: IOException) {
- println(e)
- Log.e(TAG, "IOException, you might not have internet connection $e")
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response")
- return@launchWhenCreated
- }
- nextPage = response.nextpage
- if (response.items!!.isNotEmpty()) {
- runOnUiThread {
- binding.searchRecycler.visibility = VISIBLE
- searchAdapter = SearchAdapter(response.items, childFragmentManager)
- binding.searchRecycler.adapter = searchAdapter
- }
- }
- addToHistory(query)
- isLoading = false
- isFetchingSearch = false
- }
- }
-
- private fun fetchNextSearchItems(query: String) {
- lifecycleScope.launchWhenCreated {
- if (!isLoading) {
- isLoading = true
- val response = try {
- RetrofitInstance.api.getSearchResultsNextPage(
- query,
- apiSearchFilter,
- nextPage!!
+ SearchSuggestionsAdapter(
+ response,
+ (activity as MainActivity).searchView
)
- } 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.response())
- return@launchWhenCreated
+ runOnUiThread {
+ if (viewModel.searchQuery.value != "") {
+ binding.suggestionsRecycler.adapter = suggestionsAdapter
+ }
}
- nextPage = response.nextpage
- searchAdapter?.updateItems(response.items!!)
- isLoading = false
}
}
- }
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
-
- override fun onResume() {
- super.onResume()
- requireActivity().window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
- }
-
- override fun onStop() {
- super.onStop()
- hideKeyboard()
+ run()
}
private fun showHistory() {
- binding.searchRecycler.visibility = GONE
- val historyList = PreferenceHelper.getHistory(requireContext())
+ val historyList = PreferenceHelper.getSearchHistory()
if (historyList.isNotEmpty()) {
- binding.historyRecycler.adapter =
+ binding.suggestionsRecycler.adapter =
SearchHistoryAdapter(
- requireContext(),
historyList,
- binding.autoCompleteTextView,
- this
+ (activity as MainActivity).searchView
)
- binding.historyRecycler.visibility = VISIBLE
- }
- }
-
- private fun addToHistory(query: String) {
- val searchHistoryEnabled =
- PreferenceHelper.getBoolean(requireContext(), "search_history_toggle", true)
- if (searchHistoryEnabled) {
- var historyList = PreferenceHelper.getHistory(requireContext())
-
- if ((historyList.isNotEmpty() && historyList.contains(query)) || query == "") {
- return
- } else {
- historyList = historyList + query
- }
-
- if (historyList.size > 10) {
- historyList = historyList.takeLast(10)
- }
-
- PreferenceHelper.saveHistory(requireContext(), historyList)
+ } else {
+ binding.suggestionsRecycler.visibility = View.GONE
+ binding.historyEmpty.visibility = View.VISIBLE
}
}
}
diff --git a/app/src/main/java/com/github/libretube/fragments/SearchResultFragment.kt b/app/src/main/java/com/github/libretube/fragments/SearchResultFragment.kt
new file mode 100644
index 000000000..76ca8506f
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/fragments/SearchResultFragment.kt
@@ -0,0 +1,138 @@
+package com.github.libretube.fragments
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.github.libretube.R
+import com.github.libretube.adapters.SearchAdapter
+import com.github.libretube.databinding.FragmentSearchResultBinding
+import com.github.libretube.extensions.BaseFragment
+import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
+import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.hideKeyboard
+import retrofit2.HttpException
+import java.io.IOException
+
+class SearchResultFragment : BaseFragment() {
+ private val TAG = "SearchResultFragment"
+ private lateinit var binding: FragmentSearchResultBinding
+
+ private var nextPage: String? = null
+ private var query: String = ""
+
+ private lateinit var searchAdapter: SearchAdapter
+ private var apiSearchFilter: String = "all"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ query = arguments?.getString("query").toString()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentSearchResultBinding.inflate(layoutInflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ // add the query to the history
+ addToHistory(query)
+
+ // filter options
+ binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ ->
+ apiSearchFilter = when (
+ binding.filterChipGroup.checkedChipId
+ ) {
+ R.id.chip_all -> "all"
+ R.id.chip_videos -> "videos"
+ R.id.chip_channels -> "channels"
+ R.id.chip_playlists -> "playlists"
+ R.id.chip_music_songs -> "music_songs"
+ R.id.chip_music_videos -> "music_videos"
+ R.id.chip_music_albums -> "music_albums"
+ R.id.chip_music_playlists -> "music_playlists"
+ else -> throw IllegalArgumentException("Filter out of range")
+ }
+ fetchSearch()
+ }
+
+ fetchSearch()
+
+ binding.searchRecycler.viewTreeObserver
+ .addOnScrollChangedListener {
+ if (!binding.searchRecycler.canScrollVertically(1)) {
+ if (nextPage != null) fetchNextSearchItems()
+ }
+ }
+ }
+
+ private fun fetchSearch() {
+ lifecycleScope.launchWhenCreated {
+ view?.let { context?.hideKeyboard(it) }
+ val response = try {
+ RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
+ } catch (e: IOException) {
+ println(e)
+ Log.e(TAG, "IOException, you might not have internet connection $e")
+ return@launchWhenCreated
+ } catch (e: HttpException) {
+ Log.e(TAG, "HttpException, unexpected response")
+ return@launchWhenCreated
+ }
+ runOnUiThread {
+ if (response.items?.isNotEmpty() == true) {
+ binding.searchRecycler.layoutManager = LinearLayoutManager(requireContext())
+ searchAdapter = SearchAdapter(response.items, childFragmentManager)
+ binding.searchRecycler.adapter = searchAdapter
+ } else {
+ binding.searchContainer.visibility = View.GONE
+ binding.noSearchResult.visibility = View.VISIBLE
+ }
+ }
+ nextPage = response.nextpage
+ }
+ }
+
+ private fun fetchNextSearchItems() {
+ lifecycleScope.launchWhenCreated {
+ val response = try {
+ RetrofitInstance.api.getSearchResultsNextPage(
+ query,
+ apiSearchFilter,
+ nextPage!!
+ )
+ } 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.response())
+ return@launchWhenCreated
+ }
+ nextPage = response.nextpage!!
+ kotlin.runCatching {
+ if (response.items?.isNotEmpty() == true) {
+ searchAdapter.updateItems(response.items.toMutableList())
+ }
+ }
+ }
+ }
+
+ private fun addToHistory(query: String) {
+ val searchHistoryEnabled =
+ PreferenceHelper.getBoolean(PreferenceKeys.SEARCH_HISTORY_TOGGLE, true)
+ if (searchHistoryEnabled && query != "") {
+ PreferenceHelper.saveToSearchHistory(query)
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt
index 6e40f18cb..ded749700 100644
--- a/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/SubscriptionsFragment.kt
@@ -5,30 +5,35 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.ProgressBar
import android.widget.Toast
import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
-import com.github.libretube.adapters.SubscriptionAdapter
import com.github.libretube.adapters.SubscriptionChannelAdapter
+import com.github.libretube.adapters.TrendingAdapter
import com.github.libretube.databinding.FragmentSubscriptionsBinding
+import com.github.libretube.extensions.BaseFragment
+import com.github.libretube.obj.StreamItem
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.SubscriptionHelper
+import com.github.libretube.util.toID
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
-class SubscriptionsFragment : Fragment() {
+class SubscriptionsFragment : BaseFragment() {
val TAG = "SubFragment"
private lateinit var binding: FragmentSubscriptionsBinding
lateinit var token: String
private var isLoaded = false
- private var subscriptionAdapter: SubscriptionAdapter? = null
+ private var subscriptionAdapter: TrendingAdapter? = null
+ private var feed: List = listOf()
+ private var sortOrder = "most_recent"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -47,69 +52,83 @@ class SubscriptionsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- token = PreferenceHelper.getToken(requireContext())
+ token = PreferenceHelper.getToken()
- if (token != "") {
- binding.loginOrRegister.visibility = View.GONE
- binding.subRefresh.isEnabled = true
+ binding.subRefresh.isEnabled = true
- binding.subProgress.visibility = View.VISIBLE
+ binding.subProgress.visibility = View.VISIBLE
- val grid = PreferenceHelper.getString(
- requireContext(),
- "grid",
- resources.getInteger(R.integer.grid_items).toString()
- )!!
- binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
- fetchFeed(binding.subFeed, binding.subProgress)
+ val grid = PreferenceHelper.getString(
+ PreferenceKeys.GRID_COLUMNS,
+ resources.getInteger(R.integer.grid_items).toString()
+ )
+ binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
+ fetchFeed()
- binding.subRefresh.setOnRefreshListener {
- fetchChannels(binding.subChannels)
- fetchFeed(binding.subFeed, binding.subProgress)
- }
-
- binding.toggleSubs.visibility = View.VISIBLE
- var loadedSubbedChannels = false
-
- binding.toggleSubs.setOnClickListener {
- binding.toggle.animate().rotationBy(180F).setDuration(100).start()
- if (!binding.subChannels.isVisible) {
- if (!loadedSubbedChannels) {
- binding.subChannels.layoutManager = LinearLayoutManager(context)
- fetchChannels(binding.subChannels)
- loadedSubbedChannels = true
- }
- binding.subChannels.visibility = View.VISIBLE
- binding.subFeed.visibility = View.GONE
- } else {
- binding.subChannels.visibility = View.GONE
- binding.subFeed.visibility = View.VISIBLE
- }
- }
-
- binding.scrollviewSub.viewTreeObserver
- .addOnScrollChangedListener {
- if (binding.scrollviewSub.getChildAt(0).bottom
- == (binding.scrollviewSub.height + binding.scrollviewSub.scrollY)
- ) {
- // scroll view is at bottom
- if (isLoaded) {
- binding.subRefresh.isRefreshing = true
- subscriptionAdapter?.updateItems()
- binding.subRefresh.isRefreshing = false
- }
- }
- }
- } else {
- binding.subRefresh.isEnabled = false
+ binding.subRefresh.setOnRefreshListener {
+ fetchChannels()
+ fetchFeed()
}
+
+ binding.sortTV.setOnClickListener {
+ showSortDialog()
+ }
+
+ binding.toggleSubs.visibility = View.VISIBLE
+ var loadedSubbedChannels = false
+
+ binding.toggleSubs.setOnClickListener {
+ if (!binding.subChannelsContainer.isVisible) {
+ if (!loadedSubbedChannels) {
+ binding.subChannels.layoutManager = LinearLayoutManager(context)
+ fetchChannels()
+ loadedSubbedChannels = true
+ }
+ binding.subChannelsContainer.visibility = View.VISIBLE
+ binding.subFeedContainer.visibility = View.GONE
+ } else {
+ binding.subChannelsContainer.visibility = View.GONE
+ binding.subFeedContainer.visibility = View.VISIBLE
+ }
+ }
+
+ binding.scrollviewSub.viewTreeObserver
+ .addOnScrollChangedListener {
+ if (binding.scrollviewSub.getChildAt(0).bottom
+ == (binding.scrollviewSub.height + binding.scrollviewSub.scrollY)
+ ) {
+ // scroll view is at bottom
+ if (isLoaded) {
+ binding.subRefresh.isRefreshing = true
+ subscriptionAdapter?.updateItems()
+ binding.subRefresh.isRefreshing = false
+ }
+ }
+ }
}
- private fun fetchFeed(feedRecView: RecyclerView, progressBar: ProgressBar) {
+ private fun showSortDialog() {
+ val sortOptions = resources.getStringArray(R.array.sortOptions)
+ val sortOptionValues = resources.getStringArray(R.array.sortOptionsValues)
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.sort)
+ .setItems(sortOptions) { _, index ->
+ binding.sortTV.text = sortOptions[index]
+ sortOrder = sortOptionValues[index]
+ showFeed()
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ }
+
+ private fun fetchFeed() {
fun run() {
lifecycleScope.launchWhenCreated {
- val response = try {
- RetrofitInstance.authApi.getFeed(token)
+ feed = try {
+ if (token != "") RetrofitInstance.authApi.getFeed(token)
+ else RetrofitInstance.authApi.getUnauthenticatedFeed(
+ SubscriptionHelper.getFormattedLocalSubscriptions()
+ )
} catch (e: IOException) {
Log.e(TAG, e.toString())
Log.e(TAG, "IOException, you might not have internet connection")
@@ -120,35 +139,46 @@ class SubscriptionsFragment : Fragment() {
} finally {
binding.subRefresh.isRefreshing = false
}
- if (response.isNotEmpty()) {
- subscriptionAdapter = SubscriptionAdapter(response, childFragmentManager)
- feedRecView.adapter = subscriptionAdapter
- subscriptionAdapter?.updateItems()
+ if (feed.isNotEmpty()) {
+ // save the last recent video to the prefs for the notification worker
+ PreferenceHelper.setLatestVideoId(feed[0].url.toID())
+ // show the feed
+ showFeed()
} else {
runOnUiThread {
- with(binding.boogh) {
- visibility = View.VISIBLE
- setImageResource(R.drawable.ic_list)
- }
- with(binding.textLike) {
- visibility = View.VISIBLE
- text = getString(R.string.emptyList)
- }
- binding.loginOrRegister.visibility = View.VISIBLE
+ binding.emptyFeed.visibility = View.VISIBLE
}
}
- progressBar.visibility = View.GONE
+ binding.subProgress.visibility = View.GONE
isLoaded = true
}
}
run()
}
- private fun fetchChannels(channelRecView: RecyclerView) {
+ private fun showFeed() {
+ // sort the feed
+ val sortedFeed = when (sortOrder) {
+ "most_recent" -> feed
+ "least_recent" -> feed.reversed()
+ "most_views" -> feed.sortedBy { it.views }.reversed()
+ "least_views" -> feed.sortedBy { it.views }
+ "channel_name_az" -> feed.sortedBy { it.uploaderName }
+ "channel_name_za" -> feed.sortedBy { it.uploaderName }.reversed()
+ else -> feed
+ }
+ subscriptionAdapter = TrendingAdapter(sortedFeed, childFragmentManager, false)
+ binding.subFeed.adapter = subscriptionAdapter
+ }
+
+ private fun fetchChannels() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
- RetrofitInstance.authApi.subscriptions(token)
+ if (token != "") RetrofitInstance.authApi.subscriptions(token)
+ else RetrofitInstance.authApi.unauthenticatedSubscriptions(
+ SubscriptionHelper.getFormattedLocalSubscriptions()
+ )
} catch (e: IOException) {
Log.e(TAG, e.toString())
Log.e(TAG, "IOException, you might not have internet connection")
@@ -160,7 +190,8 @@ class SubscriptionsFragment : Fragment() {
binding.subRefresh.isRefreshing = false
}
if (response.isNotEmpty()) {
- channelRecView.adapter = SubscriptionChannelAdapter(response.toMutableList())
+ binding.subChannels.adapter =
+ SubscriptionChannelAdapter(response.toMutableList())
} else {
Toast.makeText(context, R.string.subscribeIsEmpty, Toast.LENGTH_SHORT).show()
}
@@ -168,10 +199,4 @@ class SubscriptionsFragment : Fragment() {
}
run()
}
-
- private fun Fragment?.runOnUiThread(action: () -> Unit) {
- this ?: return
- if (!isAdded) return // Fragment not attached to an Activity
- activity?.runOnUiThread(action)
- }
}
diff --git a/app/src/main/java/com/github/libretube/fragments/WatchHistoryFragment.kt b/app/src/main/java/com/github/libretube/fragments/WatchHistoryFragment.kt
index dfee74fae..2aa4ad123 100644
--- a/app/src/main/java/com/github/libretube/fragments/WatchHistoryFragment.kt
+++ b/app/src/main/java/com/github/libretube/fragments/WatchHistoryFragment.kt
@@ -4,13 +4,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.adapters.WatchHistoryAdapter
import com.github.libretube.databinding.FragmentWatchHistoryBinding
+import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper
-class WatchHistoryFragment : Fragment() {
+class WatchHistoryFragment : BaseFragment() {
private val TAG = "WatchHistoryFragment"
private lateinit var binding: FragmentWatchHistoryBinding
@@ -26,20 +28,58 @@ class WatchHistoryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val watchHistory = PreferenceHelper.getWatchHistory(requireContext())
- val watchHistoryAdapter = WatchHistoryAdapter(watchHistory, childFragmentManager)
- binding.watchHistoryRecView.adapter = watchHistoryAdapter
+ val watchHistory = PreferenceHelper.getWatchHistory()
- binding.clearHistory.setOnClickListener {
- PreferenceHelper.removePreference(requireContext(), "watch_history")
- watchHistoryAdapter.clear()
+ if (watchHistory.isEmpty()) return
+
+ // reversed order
+ binding.watchHistoryRecView.layoutManager = LinearLayoutManager(requireContext()).apply {
+ reverseLayout = true
+ stackFromEnd = true
}
- // reverse order
- val linearLayoutManager = LinearLayoutManager(view.context)
- linearLayoutManager.reverseLayout = true
- linearLayoutManager.stackFromEnd = true
+ val watchHistoryAdapter = WatchHistoryAdapter(
+ watchHistory,
+ childFragmentManager
+ )
- binding.watchHistoryRecView.layoutManager = linearLayoutManager
+ val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
+ 0,
+ ItemTouchHelper.LEFT
+ ) {
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ return false
+ }
+
+ override fun onSwiped(
+ viewHolder: RecyclerView.ViewHolder,
+ direction: Int
+ ) {
+ val position = viewHolder.absoluteAdapterPosition
+ watchHistoryAdapter.removeFromWatchHistory(position)
+ }
+ }
+
+ val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
+ itemTouchHelper.attachToRecyclerView(binding.watchHistoryRecView)
+
+ // observe changes
+ watchHistoryAdapter.registerAdapterDataObserver(object :
+ RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ if (watchHistoryAdapter.itemCount == 0) {
+ binding.watchHistoryRecView.visibility = View.GONE
+ binding.historyEmpty.visibility = View.VISIBLE
+ }
+ }
+ })
+
+ binding.watchHistoryRecView.adapter = watchHistoryAdapter
+ binding.historyEmpty.visibility = View.GONE
+ binding.watchHistoryRecView.visibility = View.VISIBLE
}
}
diff --git a/app/src/main/java/com/github/libretube/interfaces/DoubleTapInterface.kt b/app/src/main/java/com/github/libretube/interfaces/DoubleTapInterface.kt
new file mode 100644
index 000000000..ba8bcd8f0
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/interfaces/DoubleTapInterface.kt
@@ -0,0 +1,5 @@
+package com.github.libretube.interfaces
+
+interface DoubleTapInterface {
+ fun onEvent(x: Float)
+}
diff --git a/app/src/main/java/com/github/libretube/interfaces/PlayerOptionsInterface.kt b/app/src/main/java/com/github/libretube/interfaces/PlayerOptionsInterface.kt
new file mode 100644
index 000000000..1f7bfbcb1
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/interfaces/PlayerOptionsInterface.kt
@@ -0,0 +1,16 @@
+package com.github.libretube.interfaces
+
+interface PlayerOptionsInterface {
+
+ fun onAutoplayClicked()
+
+ fun onCaptionClicked()
+
+ fun onQualityClicked()
+
+ fun onPlaybackSpeedClicked()
+
+ fun onAspectRatioClicked()
+
+ fun onRepeatModeClicked()
+}
diff --git a/app/src/main/java/com/github/libretube/models/SearchViewModel.kt b/app/src/main/java/com/github/libretube/models/SearchViewModel.kt
new file mode 100644
index 000000000..6a6690eb8
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/models/SearchViewModel.kt
@@ -0,0 +1,12 @@
+package com.github.libretube.models
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class SearchViewModel : ViewModel() {
+ var searchQuery = MutableLiveData()
+
+ fun setQuery(query: String?) {
+ this.searchQuery.value = query
+ }
+}
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 eb47218b3..38120cc81 100644
--- a/app/src/main/java/com/github/libretube/obj/ChapterSegment.kt
+++ b/app/src/main/java/com/github/libretube/obj/ChapterSegment.kt
@@ -4,9 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class ChapterSegment(
- var title: String?,
- var image: String?,
- var start: Long?
-) {
- constructor() : this("", "", -1)
-}
+ var title: String? = null,
+ var image: String? = null,
+ var start: Long? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/Comment.kt b/app/src/main/java/com/github/libretube/obj/Comment.kt
index 40eb6689e..5266c0d47 100644
--- a/app/src/main/java/com/github/libretube/obj/Comment.kt
+++ b/app/src/main/java/com/github/libretube/obj/Comment.kt
@@ -4,17 +4,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Comment(
- val author: String?,
- val commentId: String?,
- val commentText: String?,
- val commentedTime: String?,
- val commentorUrl: String?,
- val repliesPage: String?,
- val hearted: Boolean?,
- val likeCount: Int?,
- val pinned: Boolean?,
- val thumbnail: String?,
- val verified: Boolean?
-) {
- constructor() : this("", "", "", "", "", "", null, 0, null, "", null)
-}
+ val author: String? = null,
+ val commentId: String? = null,
+ val commentText: String? = null,
+ val commentedTime: String? = null,
+ val commentorUrl: String? = null,
+ val repliesPage: String? = null,
+ val hearted: Boolean? = null,
+ val likeCount: Int? = null,
+ val pinned: Boolean? = null,
+ val thumbnail: String? = null,
+ val verified: Boolean? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/CommentsPage.kt b/app/src/main/java/com/github/libretube/obj/CommentsPage.kt
index 4ba48f667..1244cc4a9 100644
--- a/app/src/main/java/com/github/libretube/obj/CommentsPage.kt
+++ b/app/src/main/java/com/github/libretube/obj/CommentsPage.kt
@@ -7,6 +7,4 @@ data class CommentsPage(
val comments: MutableList = arrayListOf(),
val disabled: Boolean? = null,
val nextpage: String? = ""
-) {
- constructor() : this(arrayListOf(), null, "")
-}
+)
diff --git a/app/src/main/java/com/github/libretube/obj/NewPipeSubscription.kt b/app/src/main/java/com/github/libretube/obj/NewPipeSubscription.kt
new file mode 100644
index 000000000..1db358271
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/obj/NewPipeSubscription.kt
@@ -0,0 +1,7 @@
+package com.github.libretube.obj
+
+data class NewPipeSubscription(
+ val name: String? = null,
+ val service_id: Int? = null,
+ val url: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/NewPipeSubscriptions.kt b/app/src/main/java/com/github/libretube/obj/NewPipeSubscriptions.kt
new file mode 100644
index 000000000..9c19551d2
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/obj/NewPipeSubscriptions.kt
@@ -0,0 +1,7 @@
+package com.github.libretube.obj
+
+data class NewPipeSubscriptions(
+ val app_version: String = "",
+ val app_version_int: Int = 0,
+ val subscriptions: List? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/PipedStream.kt b/app/src/main/java/com/github/libretube/obj/PipedStream.kt
index 843698737..59109a210 100644
--- a/app/src/main/java/com/github/libretube/obj/PipedStream.kt
+++ b/app/src/main/java/com/github/libretube/obj/PipedStream.kt
@@ -4,20 +4,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class PipedStream(
- var url: String?,
- var format: String?,
- var quality: String?,
- var mimeType: String?,
- var codec: String?,
- var videoOnly: Boolean?,
- var bitrate: Int?,
- var initStart: Int?,
- var initEnd: Int?,
- var indexStart: Int?,
- var indexEnd: Int?,
- var width: Int?,
- var height: Int?,
- var fps: Int?
-) {
- constructor() : this("", "", "", "", "", null, -1, -1, -1, -1, -1, -1, -1, -1)
-}
+ var url: String? = null,
+ var format: String? = null,
+ var quality: String? = null,
+ var mimeType: String? = null,
+ var codec: String? = null,
+ var videoOnly: Boolean? = null,
+ var bitrate: Int? = null,
+ var initStart: Int? = null,
+ var initEnd: Int? = null,
+ var indexStart: Int? = null,
+ var indexEnd: Int? = null,
+ var width: Int? = null,
+ var height: Int? = null,
+ var fps: Int? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/SearchItem.kt b/app/src/main/java/com/github/libretube/obj/SearchItem.kt
index 43006239b..526ecc071 100644
--- a/app/src/main/java/com/github/libretube/obj/SearchItem.kt
+++ b/app/src/main/java/com/github/libretube/obj/SearchItem.kt
@@ -4,25 +4,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class SearchItem(
- var url: String?,
- var thumbnail: String?,
- var uploaderName: String?,
- var uploaded: Long?,
- var shortDescription: String?,
+ var url: String? = null,
+ var thumbnail: String? = null,
+ var uploaderName: String? = null,
+ var uploaded: Long? = null,
+ var shortDescription: String? = null,
// Video only attributes
- var title: String?,
- var uploaderUrl: String?,
- var uploaderAvatar: String?,
- var uploadedDate: String?,
- var duration: Long?,
- var views: Long?,
- var uploaderVerified: Boolean?,
+ var title: String? = null,
+ var uploaderUrl: String? = null,
+ var uploaderAvatar: String? = null,
+ var uploadedDate: String? = null,
+ var duration: Long? = null,
+ var views: Long? = null,
+ var uploaderVerified: Boolean? = null,
// Channel and Playlist attributes
var name: String? = null,
var description: String? = null,
var subscribers: Long? = -1,
var videos: Long? = -1,
var verified: Boolean? = null
-) {
- constructor() : this("", "", "", 0, "", "", "", "", "", 0, 0, null)
-}
+)
diff --git a/app/src/main/java/com/github/libretube/obj/Segment.kt b/app/src/main/java/com/github/libretube/obj/Segment.kt
index 85ab9d344..b4eff6aec 100644
--- a/app/src/main/java/com/github/libretube/obj/Segment.kt
+++ b/app/src/main/java/com/github/libretube/obj/Segment.kt
@@ -4,9 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Segment(
- val actionType: String?,
- val category: String?,
- val segment: List?
-) {
- constructor() : this("", "", arrayListOf())
-}
+ val actionType: String? = null,
+ val category: String? = null,
+ val segment: List? = arrayListOf()
+)
diff --git a/app/src/main/java/com/github/libretube/obj/Segments.kt b/app/src/main/java/com/github/libretube/obj/Segments.kt
index af3628cdc..e7c1b1297 100644
--- a/app/src/main/java/com/github/libretube/obj/Segments.kt
+++ b/app/src/main/java/com/github/libretube/obj/Segments.kt
@@ -5,6 +5,4 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Segments(
val segments: MutableList = arrayListOf()
-) {
- constructor() : this(arrayListOf())
-}
+)
diff --git a/app/src/main/java/com/github/libretube/obj/SponsorBlockPrefs.kt b/app/src/main/java/com/github/libretube/obj/SponsorBlockPrefs.kt
deleted file mode 100644
index 73118d5ca..000000000
--- a/app/src/main/java/com/github/libretube/obj/SponsorBlockPrefs.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.github.libretube.obj
-
-class SponsorBlockPrefs(
- var sponsorBlockEnabled: Boolean = false,
- var sponsorNotificationsEnabled: Boolean = false,
- var sponsorsEnabled: Boolean = false,
- var selfPromoEnabled: Boolean = false,
- var interactionEnabled: Boolean = false,
- var introEnabled: Boolean = false,
- var outroEnabled: Boolean = false,
- var fillerEnabled: Boolean = false,
- var musicOffTopicEnabled: Boolean = false,
- var previewEnabled: Boolean = false
-)
diff --git a/app/src/main/java/com/github/libretube/obj/StreamItem.kt b/app/src/main/java/com/github/libretube/obj/StreamItem.kt
index f22e4d256..b8eb2ee9e 100644
--- a/app/src/main/java/com/github/libretube/obj/StreamItem.kt
+++ b/app/src/main/java/com/github/libretube/obj/StreamItem.kt
@@ -4,18 +4,16 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class StreamItem(
- var url: String?,
- var title: String?,
- var thumbnail: String?,
- var uploaderName: String?,
- var uploaderUrl: String?,
- var uploaderAvatar: String?,
- var uploadedDate: String?,
- var duration: Long?,
- var views: Long?,
- var uploaderVerified: Boolean?,
- var uploaded: Long?,
- var shortDescription: String?
-) {
- constructor() : this("", "", "", "", "", "", "", 0, 0, null, 0, "")
-}
+ var url: String? = null,
+ var title: String? = null,
+ var thumbnail: String? = null,
+ var uploaderName: String? = null,
+ var uploaderUrl: String? = null,
+ var uploaderAvatar: String? = null,
+ var uploadedDate: String? = null,
+ var duration: Long? = null,
+ var views: Long? = null,
+ var uploaderVerified: Boolean? = null,
+ var uploaded: Long? = null,
+ var shortDescription: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/Streams.kt b/app/src/main/java/com/github/libretube/obj/Streams.kt
index 34613b73e..b497859ac 100644
--- a/app/src/main/java/com/github/libretube/obj/Streams.kt
+++ b/app/src/main/java/com/github/libretube/obj/Streams.kt
@@ -15,7 +15,7 @@ data class Streams(
val dash: String?,
val lbryId: String?,
val uploaderVerified: Boolean?,
- val duration: Int?,
+ val duration: Long?,
val views: Long?,
val likes: Long?,
val dislikes: Long?,
diff --git a/app/src/main/java/com/github/libretube/obj/Subtitle.kt b/app/src/main/java/com/github/libretube/obj/Subtitle.kt
index fa80f697a..1bcdc6957 100644
--- a/app/src/main/java/com/github/libretube/obj/Subtitle.kt
+++ b/app/src/main/java/com/github/libretube/obj/Subtitle.kt
@@ -4,11 +4,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Subtitle(
- val url: String?,
- val mimeType: String?,
- val name: String?,
- val code: String?,
- val autoGenerated: Boolean?
-) {
- constructor() : this("", "", "", "", null)
-}
+ val url: String? = null,
+ val mimeType: String? = null,
+ val name: String? = null,
+ val code: String? = null,
+ val autoGenerated: Boolean? = null
+)
diff --git a/app/src/main/java/com/github/libretube/obj/UpdateInfo.kt b/app/src/main/java/com/github/libretube/obj/UpdateInfo.kt
deleted file mode 100644
index 91808912b..000000000
--- a/app/src/main/java/com/github/libretube/obj/UpdateInfo.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.libretube.obj
-
-// data class for the update info, required to return the data
-data class UpdateInfo(
- val updateUrl: String,
- val tagName: String
-)
diff --git a/app/src/main/java/com/github/libretube/obj/WatchHistoryItem.kt b/app/src/main/java/com/github/libretube/obj/WatchHistoryItem.kt
index b8eab4111..f8b436d5f 100644
--- a/app/src/main/java/com/github/libretube/obj/WatchHistoryItem.kt
+++ b/app/src/main/java/com/github/libretube/obj/WatchHistoryItem.kt
@@ -1,12 +1,12 @@
package com.github.libretube.obj
data class WatchHistoryItem(
- val videoId: String?,
- val title: String?,
- val uploadDate: String?,
- val uploader: String?,
- val uploaderUrl: String?,
- val uploaderAvatar: String?,
- val thumbnailUrl: String?,
- val duration: Int?
+ val videoId: String? = null,
+ val title: String? = null,
+ val uploadDate: String? = null,
+ val uploader: String? = null,
+ val uploaderUrl: String? = null,
+ val uploaderAvatar: String? = null,
+ val thumbnailUrl: String? = null,
+ val duration: Long? = null
)
diff --git a/app/src/main/java/com/github/libretube/obj/WatchPosition.kt b/app/src/main/java/com/github/libretube/obj/WatchPosition.kt
index b7b000310..e98b4e812 100644
--- a/app/src/main/java/com/github/libretube/obj/WatchPosition.kt
+++ b/app/src/main/java/com/github/libretube/obj/WatchPosition.kt
@@ -1,6 +1,6 @@
package com.github.libretube.obj
data class WatchPosition(
- val videoId: String,
- val position: Long
+ val videoId: String = "",
+ val position: Long = 0L
)
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 750fe5f9a..ceeb221bf 100644
--- a/app/src/main/java/com/github/libretube/preferences/AdvancedSettings.kt
+++ b/app/src/main/java/com/github/libretube/preferences/AdvancedSettings.kt
@@ -2,13 +2,12 @@ package com.github.libretube.preferences
import android.os.Bundle
import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
-import com.github.libretube.dialogs.RequireRestartDialog
+import com.github.libretube.views.MaterialPreferenceFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-class AdvancedSettings : PreferenceFragmentCompat() {
+class AdvancedSettings : MaterialPreferenceFragment() {
val TAG = "AdvancedSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -17,22 +16,7 @@ class AdvancedSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.advanced))
- // clear search history
- val clearHistory = findPreference("clear_history")
- clearHistory?.setOnPreferenceClickListener {
- PreferenceHelper.removePreference(requireContext(), "search_history")
- true
- }
-
- // clear watch history and positions
- val clearWatchHistory = findPreference("clear_watch_history")
- clearWatchHistory?.setOnPreferenceClickListener {
- PreferenceHelper.removePreference(requireContext(), "watch_history")
- PreferenceHelper.removePreference(requireContext(), "watch_positions")
- true
- }
-
- val resetSettings = findPreference("reset_settings")
+ val resetSettings = findPreference(PreferenceKeys.RESET_SETTINGS)
resetSettings?.setOnPreferenceClickListener {
showResetDialog()
true
@@ -41,19 +25,18 @@ class AdvancedSettings : PreferenceFragmentCompat() {
private fun showResetDialog() {
MaterialAlertDialogBuilder(requireContext())
- .setPositiveButton(R.string.reset) { _, _ ->
- // clear default preferences
- PreferenceHelper.clearPreferences(requireContext())
-
- // clear login token
- PreferenceHelper.setToken(requireContext(), "")
-
- val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
- }
- .setNegativeButton(getString(R.string.cancel)) { _, _ -> }
.setTitle(R.string.reset)
.setMessage(R.string.reset_message)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.reset) { _, _ ->
+ // clear default preferences
+ PreferenceHelper.clearPreferences()
+
+ // clear login token
+ PreferenceHelper.setToken("")
+
+ activity?.recreate()
+ }
.show()
}
}
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 5a6dcc5e9..7c5ce8f5d 100644
--- a/app/src/main/java/com/github/libretube/preferences/AppearanceSettings.kt
+++ b/app/src/main/java/com/github/libretube/preferences/AppearanceSettings.kt
@@ -1,16 +1,21 @@
package com.github.libretube.preferences
+import android.content.ActivityNotFoundException
+import android.content.Intent
import android.os.Bundle
+import android.provider.Settings
+import android.widget.Toast
import androidx.preference.ListPreference
-import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.dialogs.RequireRestartDialog
import com.github.libretube.util.ThemeHelper
+import com.github.libretube.views.MaterialPreferenceFragment
import com.google.android.material.color.DynamicColors
-class AppearanceSettings : PreferenceFragmentCompat() {
+class AppearanceSettings : MaterialPreferenceFragment() {
private val TAG = "AppearanceSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.appearance_settings, rootKey)
@@ -18,52 +23,59 @@ class AppearanceSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.appearance))
- val themeToggle = findPreference("theme_toggle")
+ val themeToggle = findPreference(PreferenceKeys.THEME_MODE)
themeToggle?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
- val pureTheme = findPreference("pure_theme")
+ val pureTheme = findPreference(PreferenceKeys.PURE_THEME)
pureTheme?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
- val accentColor = findPreference("accent_color")
+ val accentColor = findPreference(PreferenceKeys.ACCENT_COLOR)
updateAccentColorValues(accentColor!!)
accentColor.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
- val iconChange = findPreference("icon_change")
+ val iconChange = findPreference(PreferenceKeys.APP_ICON)
iconChange?.setOnPreferenceChangeListener { _, newValue ->
ThemeHelper.changeIcon(requireContext(), newValue.toString())
true
}
- val gridColumns = findPreference("grid")
- gridColumns?.setOnPreferenceChangeListener { _, _ ->
- val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
- true
- }
-
- val hideTrending = findPreference("hide_trending_page")
- hideTrending?.setOnPreferenceChangeListener { _, _ ->
- val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
- true
- }
-
- val labelVisibilityMode = findPreference("label_visibility")
+ val labelVisibilityMode = findPreference(PreferenceKeys.LABEL_VISIBILITY)
labelVisibilityMode?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
+ true
+ }
+
+ val systemCaptionStyle =
+ findPreference(PreferenceKeys.SYSTEM_CAPTION_STYLE)
+ val captionSettings = findPreference(PreferenceKeys.CAPTION_SETTINGS)
+
+ captionSettings?.isVisible =
+ PreferenceHelper.getBoolean(PreferenceKeys.SYSTEM_CAPTION_STYLE, true)
+ systemCaptionStyle?.setOnPreferenceChangeListener { _, newValue ->
+ captionSettings?.isVisible = newValue as Boolean
+ true
+ }
+
+ captionSettings?.setOnPreferenceClickListener {
+ try {
+ val captionSettingsIntent = Intent(Settings.ACTION_CAPTIONING_SETTINGS)
+ startActivity(captionSettingsIntent)
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show()
+ }
true
}
}
diff --git a/app/src/main/java/com/github/libretube/preferences/GeneralSettings.kt b/app/src/main/java/com/github/libretube/preferences/GeneralSettings.kt
new file mode 100644
index 000000000..fe88d4504
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/preferences/GeneralSettings.kt
@@ -0,0 +1,48 @@
+package com.github.libretube.preferences
+
+import android.os.Bundle
+import androidx.preference.ListPreference
+import androidx.preference.SwitchPreferenceCompat
+import com.github.libretube.R
+import com.github.libretube.activities.SettingsActivity
+import com.github.libretube.dialogs.RequireRestartDialog
+import com.github.libretube.views.MaterialPreferenceFragment
+
+class GeneralSettings : MaterialPreferenceFragment() {
+ val TAG = "SettingsFragment"
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.general_settings, rootKey)
+
+ val settingsActivity = activity as SettingsActivity
+ settingsActivity.changeTopBarText(getString(R.string.general))
+
+ val language = findPreference("language")
+ language?.setOnPreferenceChangeListener { _, _ ->
+ val restartDialog = RequireRestartDialog()
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
+ true
+ }
+
+ val autoRotation = findPreference(PreferenceKeys.AUTO_ROTATION)
+ autoRotation?.setOnPreferenceChangeListener { _, _ ->
+ val restartDialog = RequireRestartDialog()
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
+ true
+ }
+
+ val hideTrending = findPreference(PreferenceKeys.HIDE_TRENDING_PAGE)
+ hideTrending?.setOnPreferenceChangeListener { _, _ ->
+ val restartDialog = RequireRestartDialog()
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
+ true
+ }
+
+ val breakReminder = findPreference(PreferenceKeys.BREAK_REMINDER)
+ breakReminder?.setOnPreferenceChangeListener { _, _ ->
+ val restartDialog = RequireRestartDialog()
+ restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
+ true
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/preferences/HistorySettings.kt b/app/src/main/java/com/github/libretube/preferences/HistorySettings.kt
new file mode 100644
index 000000000..12c57e1e5
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/preferences/HistorySettings.kt
@@ -0,0 +1,51 @@
+package com.github.libretube.preferences
+
+import android.os.Bundle
+import androidx.preference.Preference
+import com.github.libretube.R
+import com.github.libretube.activities.SettingsActivity
+import com.github.libretube.views.MaterialPreferenceFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+class HistorySettings : MaterialPreferenceFragment() {
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.history_settings, rootKey)
+
+ val settingsActivity = activity as SettingsActivity
+ settingsActivity.changeTopBarText(getString(R.string.history))
+
+ // clear search history
+ val clearHistory = findPreference(PreferenceKeys.CLEAR_SEARCH_HISTORY)
+ clearHistory?.setOnPreferenceClickListener {
+ showClearDialog(R.string.clear_history, "search_history")
+ true
+ }
+
+ // clear watch history and positions
+ val clearWatchHistory = findPreference(PreferenceKeys.CLEAR_WATCH_HISTORY)
+ clearWatchHistory?.setOnPreferenceClickListener {
+ showClearDialog(R.string.clear_history, "watch_history")
+ true
+ }
+
+ // clear watch positions
+ val clearWatchPositions = findPreference(PreferenceKeys.CLEAR_WATCH_POSITIONS)
+ clearWatchPositions?.setOnPreferenceClickListener {
+ showClearDialog(R.string.reset_watch_positions, "watch_positions")
+ true
+ }
+ }
+
+ private fun showClearDialog(title: Int, preferenceKey: String) {
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(title)
+ .setMessage(R.string.irreversible)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.okay) { _, _ ->
+ // clear the selected preference preferences
+ PreferenceHelper.removePreference(preferenceKey)
+ }
+ .show()
+ }
+}
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 084d661af..12c2a9381 100644
--- a/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt
+++ b/app/src/main/java/com/github/libretube/preferences/InstanceSettings.kt
@@ -1,23 +1,15 @@
package com.github.libretube.preferences
-import android.Manifest
-import android.content.ContentResolver
import android.content.Intent
-import android.content.pm.PackageManager
import android.net.Uri
-import android.os.Build
import android.os.Bundle
-import android.text.TextUtils
-import android.util.Log
import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
@@ -25,87 +17,33 @@ 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.ImportHelper
+import com.github.libretube.util.PermissionHelper
import com.github.libretube.util.RetrofitInstance
-import org.json.JSONObject
-import org.json.JSONTokener
-import retrofit2.HttpException
-import java.io.IOException
-import java.io.InputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipInputStream
+import com.github.libretube.views.MaterialPreferenceFragment
-class InstanceSettings : PreferenceFragmentCompat() {
+class InstanceSettings : MaterialPreferenceFragment() {
val TAG = "InstanceSettings"
+ /**
+ * result listeners for importing and exporting subscriptions
+ */
+ private lateinit var getContent: ActivityResultLauncher
+ private lateinit var createFile: ActivityResultLauncher
+
override fun onCreate(savedInstanceState: Bundle?) {
- MainSettings.getContent =
- registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
- if (uri != null) {
- try {
- // Open a specific media item using ParcelFileDescriptor.
- val resolver: ContentResolver =
- requireActivity()
- .contentResolver
-
- // "rw" for read-and-write;
- // "rwt" for truncating or overwriting existing file contents.
- // val readOnlyMode = "r"
- // uri - I have got from onActivityResult
- val type = resolver.getType(uri)
-
- var inputStream: InputStream? = resolver.openInputStream(uri)
- val channels = ArrayList()
- if (type == "application/json") {
- val json = inputStream?.bufferedReader()?.readLines()?.get(0)
- val jsonObject = JSONTokener(json).nextValue() as JSONObject
- Log.e(TAG, jsonObject.getJSONArray("subscriptions").toString())
- for (
- i in 0 until jsonObject.getJSONArray("subscriptions")
- .length()
- ) {
- var url =
- jsonObject.getJSONArray("subscriptions").getJSONObject(i)
- .getString("url")
- url = url.replace("https://www.youtube.com/channel/", "")
- Log.e(TAG, url)
- channels.add(url)
- }
- } else {
- if (type == "application/zip") {
- val zis = ZipInputStream(inputStream)
- var entry: ZipEntry? = zis.nextEntry
- while (entry != null) {
- if (entry.name.endsWith(".csv")) {
- inputStream = zis
- break
- }
- entry = zis.nextEntry
- }
- }
-
- inputStream?.bufferedReader()?.readLines()?.forEach {
- if (it.isNotBlank()) {
- val channelId = it.substringBefore(",")
- if (channelId.length == 24) {
- channels.add(channelId)
- }
- }
- }
- }
- inputStream?.close()
-
- subscribe(channels)
- } catch (e: Exception) {
- Log.e(TAG, e.toString())
- Toast.makeText(
- context,
- R.string.error,
- Toast.LENGTH_SHORT
- ).show()
- }
- }
+ getContent =
+ registerForActivityResult(
+ ActivityResultContracts.GetContent()
+ ) { uri: Uri? ->
+ ImportHelper(requireActivity()).importSubscriptions(uri)
}
+ createFile = registerForActivityResult(
+ ActivityResultContracts.CreateDocument()
+ ) { uri: Uri? ->
+ ImportHelper(requireActivity()).exportSubscriptions(uri)
+ }
+
super.onCreate(savedInstanceState)
}
@@ -115,25 +53,24 @@ class InstanceSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.instance))
- val instance = findPreference("selectInstance")
+ val instance = findPreference(PreferenceKeys.FETCH_INSTANCE)
// fetchInstance()
initCustomInstances(instance!!)
instance.setOnPreferenceChangeListener { _, newValue ->
- val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
RetrofitInstance.url = newValue.toString()
- if (!PreferenceHelper.getBoolean(requireContext(), "auth_instance_toggle", false)) {
+ if (!PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
RetrofitInstance.authUrl = newValue.toString()
logout()
}
RetrofitInstance.lazyMgr.reset()
+ activity?.recreate()
true
}
- val authInstance = findPreference("selectAuthInstance")
+ val authInstance = findPreference(PreferenceKeys.AUTH_INSTANCE)
initCustomInstances(authInstance!!)
// hide auth instance if option deselected
- if (!PreferenceHelper.getBoolean(requireContext(), "auth_instance_toggle", false)) {
+ if (!PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
authInstance.isVisible = false
}
authInstance.setOnPreferenceChangeListener { _, newValue ->
@@ -141,226 +78,132 @@ class InstanceSettings : PreferenceFragmentCompat() {
RetrofitInstance.authUrl = newValue.toString()
RetrofitInstance.lazyMgr.reset()
logout()
- val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
+ activity?.recreate()
true
}
- val authInstanceToggle = findPreference("auth_instance_toggle")
+ val authInstanceToggle =
+ findPreference(PreferenceKeys.AUTH_INSTANCE_TOGGLE)
authInstanceToggle?.setOnPreferenceChangeListener { _, newValue ->
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")
+ RetrofitInstance.lazyMgr.reset()
+ activity?.recreate()
true
}
- val customInstance = findPreference("customInstance")
+ val customInstance = findPreference(PreferenceKeys.CUSTOM_INSTANCE)
customInstance?.setOnPreferenceClickListener {
val newFragment = CustomInstanceDialog()
- newFragment.show(childFragmentManager, "CustomInstanceDialog")
+ newFragment.show(childFragmentManager, CustomInstanceDialog::class.java.name)
true
}
- val clearCustomInstances = findPreference("clearCustomInstances")
+ val clearCustomInstances = findPreference(PreferenceKeys.CLEAR_CUSTOM_INSTANCES)
clearCustomInstances?.setOnPreferenceClickListener {
- PreferenceHelper.removePreference(requireContext(), "customInstances")
+ PreferenceHelper.removePreference("customInstances")
val intent = Intent(context, SettingsActivity::class.java)
startActivity(intent)
true
}
- val login = findPreference("login_register")
- val token = PreferenceHelper.getToken(requireContext())
+ val login = findPreference(PreferenceKeys.LOGIN_REGISTER)
+ val token = PreferenceHelper.getToken()
if (token != "") login?.setTitle(R.string.logout)
login?.setOnPreferenceClickListener {
if (token == "") {
val newFragment = LoginDialog()
- newFragment.show(childFragmentManager, "Login")
+ newFragment.show(childFragmentManager, LoginDialog::class.java.name)
} else {
val newFragment = LogoutDialog()
- newFragment.show(childFragmentManager, "Logout")
+ newFragment.show(childFragmentManager, LogoutDialog::class.java.name)
}
true
}
- val deleteAccount = findPreference("delete_account")
+ val deleteAccount = findPreference(PreferenceKeys.DELETE_ACCOUNT)
deleteAccount?.setOnPreferenceClickListener {
- val token = PreferenceHelper.getToken(requireContext())
+ val token = PreferenceHelper.getToken()
if (token != "") {
val newFragment = DeleteAccountDialog()
- newFragment.show(childFragmentManager, "DeleteAccountDialog")
+ newFragment.show(childFragmentManager, DeleteAccountDialog::class.java.name)
} else {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
}
true
}
- val importFromYt = findPreference("import_from_yt")
- importFromYt?.setOnPreferenceClickListener {
- importSubscriptions()
+ val importSubscriptions = findPreference(PreferenceKeys.IMPORT_SUBS)
+ importSubscriptions?.setOnPreferenceClickListener {
+ // check StorageAccess
+ val accessGranted =
+ PermissionHelper.isStoragePermissionGranted(requireActivity())
+ // import subscriptions
+ if (accessGranted) getContent.launch("*/*")
+ // request permissions if not granted
+ else PermissionHelper.requestReadWrite(requireActivity())
+ true
+ }
+
+ val exportSubscriptions = findPreference(PreferenceKeys.EXPORT_SUBS)
+ exportSubscriptions?.setOnPreferenceClickListener {
+ createFile.launch("subscriptions.json")
true
}
}
private fun initCustomInstances(instancePref: ListPreference) {
- val customInstances = PreferenceHelper.getCustomInstances(requireContext())
-
- var instanceNames = resources.getStringArray(R.array.instances)
- var instanceValues = resources.getStringArray(R.array.instancesValue)
- customInstances.forEach { instance ->
- instanceNames += instance.name
- instanceValues += instance.apiUrl
- }
-
- // add custom instances to the list preference
- instancePref.entries = instanceNames
- instancePref.entryValues = instanceValues
- instancePref.summaryProvider =
- Preference.SummaryProvider { preference ->
- val text = preference.entry
- if (TextUtils.isEmpty(text)) {
- "kavin.rocks (Official)"
- } else {
- text
- }
- }
- }
-
- private fun logout() {
- PreferenceHelper.setToken(requireContext(), "")
- Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
- }
-
- private fun fetchInstance() {
lifecycleScope.launchWhenCreated {
+ val customInstances = PreferenceHelper.getCustomInstances()
+
+ val instanceNames = arrayListOf()
+ val instanceValues = arrayListOf()
+
+ // fetch official public instances
+
val response = try {
RetrofitInstance.api.getInstances("https://instances.tokhmi.xyz/")
- } catch (e: IOException) {
- println(e)
- Log.e("settings", "IOException, you might not have internet connection")
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e("settings", "HttpException, unexpected response $e")
- return@launchWhenCreated
} catch (e: Exception) {
- Log.e("settings", e.toString())
- return@launchWhenCreated
- }
- val listEntries: MutableList = ArrayList()
- val listEntryValues: MutableList = ArrayList()
- for (item in response) {
- listEntries.add(item.name!!)
- listEntryValues.add(item.api_url!!)
+ e.printStackTrace()
+ emptyList()
}
- // add custom instances to the list
+ response.forEach {
+ if (it.name != null && it.api_url != null) {
+ instanceNames += it.name!!
+ instanceValues += it.api_url!!
+ }
+ }
+
+ customInstances.forEach { instance ->
+ instanceNames += instance.name
+ instanceValues += instance.apiUrl
+ }
- val entries = listEntries.toTypedArray()
- val entryValues = listEntryValues.toTypedArray()
runOnUiThread {
- val instance = findPreference("selectInstance")
- instance?.entries = entries
- instance?.entryValues = entryValues
- instance?.summaryProvider =
+ // add custom instances to the list preference
+ instancePref.entries = instanceNames.toTypedArray()
+ instancePref.entryValues = instanceValues.toTypedArray()
+ instancePref.summaryProvider =
Preference.SummaryProvider { preference ->
- val text = preference.entry
- if (TextUtils.isEmpty(text)) {
- "kavin.rocks (Official)"
- } else {
- text
- }
+ preference.entry
}
}
}
}
+ private fun logout() {
+ PreferenceHelper.setToken("")
+ Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
+ }
+
private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return
if (!isAdded) return // Fragment not attached to an Activity
activity?.runOnUiThread(action)
}
-
- private fun importSubscriptions() {
- val token = PreferenceHelper.getToken(requireContext())
- // check StorageAccess
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Log.d("myz", "" + Build.VERSION.SDK_INT)
- if (ContextCompat.checkSelfPermission(
- this.requireContext(),
- Manifest.permission.READ_EXTERNAL_STORAGE
- )
- != PackageManager.PERMISSION_GRANTED
- ) {
- ActivityCompat.requestPermissions(
- this.requireActivity(),
- arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.MANAGE_EXTERNAL_STORAGE
- ),
- 1
- ) // permission request code is just an int
- } else if (token != "") {
- MainSettings.getContent.launch("*/*")
- } else {
- Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
- }
- } else {
- if (ActivityCompat.checkSelfPermission(
- requireContext(),
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED ||
- ActivityCompat.checkSelfPermission(
- requireContext(),
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- ActivityCompat.requestPermissions(
- this.requireActivity(),
- arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ),
- 1
- )
- } else if (token != "") {
- MainSettings.getContent.launch("*/*")
- } else {
- Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- private fun subscribe(channels: List) {
- fun run() {
- lifecycleScope.launchWhenCreated {
- val response = try {
- val token = PreferenceHelper.getToken(requireContext())
- RetrofitInstance.authApi.importSubscriptions(
- false,
- token,
- channels
- )
- } catch (e: IOException) {
- Log.e(TAG, "IOException, you might not have internet connection")
- return@launchWhenCreated
- } catch (e: HttpException) {
- Log.e(TAG, "HttpException, unexpected response$e")
- return@launchWhenCreated
- }
- if (response.message == "ok") {
- Toast.makeText(
- context,
- R.string.importsuccess,
- Toast.LENGTH_SHORT
- ).show()
- }
- }
- }
- run()
- }
}
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 f131e0ecd..dbad3d703 100644
--- a/app/src/main/java/com/github/libretube/preferences/MainSettings.kt
+++ b/app/src/main/java/com/github/libretube/preferences/MainSettings.kt
@@ -1,38 +1,29 @@
package com.github.libretube.preferences
import android.os.Bundle
-import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment
-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.dialogs.RequireRestartDialog
-import com.github.libretube.util.ThemeHelper
-import com.github.libretube.util.checkUpdate
+import com.github.libretube.activities.SettingsActivity
+import com.github.libretube.dialogs.UpdateDialog
+import com.github.libretube.update.UpdateChecker
+import com.github.libretube.views.MaterialPreferenceFragment
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
-class MainSettings : PreferenceFragmentCompat() {
+class MainSettings : MaterialPreferenceFragment() {
val TAG = "SettingsFragment"
- companion object {
- lateinit var getContent: ActivityResultLauncher
- }
-
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
- val region = findPreference("region")
- region?.setOnPreferenceChangeListener { _, _ ->
- val restartDialog = RequireRestartDialog()
- restartDialog.show(childFragmentManager, "RequireRestartDialog")
- true
- }
-
- val language = findPreference("language")
- language?.setOnPreferenceChangeListener { _, _ ->
- ThemeHelper.restartMainActivity(requireContext())
+ val general = findPreference("general")
+ general?.setOnPreferenceClickListener {
+ val newFragment = GeneralSettings()
+ navigateToSettingsFragment(newFragment)
true
}
@@ -64,6 +55,20 @@ class MainSettings : PreferenceFragmentCompat() {
true
}
+ val history = findPreference("history")
+ history?.setOnPreferenceClickListener {
+ val newFragment = HistorySettings()
+ navigateToSettingsFragment(newFragment)
+ true
+ }
+
+ val notifications = findPreference("notifications")
+ notifications?.setOnPreferenceClickListener {
+ val newFragment = NotificationSettings()
+ navigateToSettingsFragment(newFragment)
+ true
+ }
+
val advanced = findPreference("advanced")
advanced?.setOnPreferenceClickListener {
val newFragment = AdvancedSettings()
@@ -72,29 +77,48 @@ class MainSettings : PreferenceFragmentCompat() {
}
val update = findPreference("update")
- update?.title = getString(R.string.version, BuildConfig.VERSION_NAME)
+
+ // set the version of the update preference
+ val versionString = if (BuildConfig.DEBUG) "${BuildConfig.VERSION_NAME} Debug"
+ else getString(R.string.version, BuildConfig.VERSION_NAME)
+ update?.title = versionString
+
+ // checking for update: yes -> dialog, no -> snackBar
update?.setOnPreferenceClickListener {
- checkUpdate(childFragmentManager)
- true
- }
-
- val about = findPreference("about")
- about?.setOnPreferenceClickListener {
- val newFragment = AboutFragment()
- navigateToSettingsFragment(newFragment)
- true
- }
-
- val community = findPreference("community")
- community?.setOnPreferenceClickListener {
- val newFragment = CommunityFragment()
- navigateToSettingsFragment(newFragment)
+ CoroutineScope(Dispatchers.IO).launch {
+ // check for update
+ val updateInfo = UpdateChecker.getLatestReleaseInfo()
+ if (updateInfo?.name == null) {
+ // request failed
+ val settingsActivity = activity as SettingsActivity
+ val snackBar = Snackbar
+ .make(
+ settingsActivity.binding.root,
+ R.string.unknown_error,
+ Snackbar.LENGTH_SHORT
+ )
+ snackBar.show()
+ } else if (BuildConfig.VERSION_NAME != updateInfo.name) {
+ // show the UpdateAvailableDialog if there's an update available
+ val updateAvailableDialog = UpdateDialog(updateInfo)
+ updateAvailableDialog.show(childFragmentManager, UpdateDialog::class.java.name)
+ } else {
+ // otherwise show the no update available snackBar
+ val settingsActivity = activity as SettingsActivity
+ val snackBar = Snackbar
+ .make(
+ settingsActivity.binding.root,
+ R.string.app_uptodate,
+ Snackbar.LENGTH_SHORT
+ )
+ snackBar.show()
+ }
+ }
true
}
}
private fun navigateToSettingsFragment(newFragment: Fragment) {
- Globals.isCurrentViewMainSettings = false
parentFragmentManager.beginTransaction()
.replace(R.id.settings, newFragment)
.commitNow()
diff --git a/app/src/main/java/com/github/libretube/preferences/NotificationSettings.kt b/app/src/main/java/com/github/libretube/preferences/NotificationSettings.kt
new file mode 100644
index 000000000..bea9048aa
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/preferences/NotificationSettings.kt
@@ -0,0 +1,42 @@
+package com.github.libretube.preferences
+
+import android.os.Bundle
+import androidx.preference.ListPreference
+import androidx.preference.SwitchPreferenceCompat
+import androidx.work.ExistingPeriodicWorkPolicy
+import com.github.libretube.R
+import com.github.libretube.activities.SettingsActivity
+import com.github.libretube.util.NotificationHelper
+import com.github.libretube.views.MaterialPreferenceFragment
+
+class NotificationSettings : MaterialPreferenceFragment() {
+ val TAG = "SettingsFragment"
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.notification_settings, rootKey)
+
+ val settingsActivity = activity as SettingsActivity
+ settingsActivity.changeTopBarText(getString(R.string.notifications))
+
+ val notificationsEnabled =
+ findPreference(PreferenceKeys.NOTIFICATION_ENABLED)
+ notificationsEnabled?.setOnPreferenceChangeListener { _, _ ->
+ updateNotificationPrefs()
+ true
+ }
+
+ val checkingFrequency = findPreference(PreferenceKeys.CHECKING_FREQUENCY)
+ checkingFrequency?.setOnPreferenceChangeListener { _, _ ->
+ updateNotificationPrefs()
+ true
+ }
+ }
+
+ private fun updateNotificationPrefs() {
+ // replace the previous queued work request
+ NotificationHelper.enqueueWork(
+ requireContext(),
+ ExistingPeriodicWorkPolicy.REPLACE
+ )
+ }
+}
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 b3428f4f7..ae1df72ec 100644
--- a/app/src/main/java/com/github/libretube/preferences/PlayerSettings.kt
+++ b/app/src/main/java/com/github/libretube/preferences/PlayerSettings.kt
@@ -2,12 +2,14 @@ package com.github.libretube.preferences
import android.os.Bundle
import androidx.preference.ListPreference
-import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
+import com.github.libretube.views.MaterialPreferenceFragment
+import java.util.*
-class PlayerSettings : PreferenceFragmentCompat() {
+class PlayerSettings : MaterialPreferenceFragment() {
val TAG = "PlayerSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -16,13 +18,14 @@ 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")
+ val playerOrientation =
+ findPreference(PreferenceKeys.FULLSCREEN_ORIENTATION)
+ val autoRotateToFullscreen =
+ findPreference(PreferenceKeys.AUTO_FULLSCREEN)
// only show the player orientation option if auto fullscreen is disabled
playerOrientation?.isEnabled != PreferenceHelper.getBoolean(
- requireContext(),
- "auto_fullscreen",
+ PreferenceKeys.AUTO_FULLSCREEN,
false
)
@@ -30,5 +33,26 @@ class PlayerSettings : PreferenceFragmentCompat() {
playerOrientation?.isEnabled = newValue != true
true
}
+
+ val defaultSubtitle = findPreference(PreferenceKeys.DEFAULT_SUBTITLE)
+ val locales: Array = Locale.getAvailableLocales()
+ val localeNames = ArrayList()
+ val localeCodes = ArrayList()
+
+ localeNames.add(context?.getString(R.string.none)!!)
+ localeCodes.add("")
+
+ locales.forEach {
+ if (!localeNames.contains(it.getDisplayLanguage())) {
+ localeNames.add(it.getDisplayLanguage())
+ localeCodes.add(it.language)
+ }
+ }
+ defaultSubtitle?.entries = localeNames.toTypedArray()
+ defaultSubtitle?.entryValues = localeCodes.toTypedArray()
+ defaultSubtitle?.summaryProvider =
+ Preference.SummaryProvider { preference ->
+ preference.entry
+ }
}
}
diff --git a/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt b/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt
index 1e00a7fd2..e53f18863 100644
--- a/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt
+++ b/app/src/main/java/com/github/libretube/preferences/PreferenceHelper.kt
@@ -3,176 +3,192 @@ package com.github.libretube.preferences
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.obj.CustomInstance
import com.github.libretube.obj.Streams
import com.github.libretube.obj.WatchHistoryItem
import com.github.libretube.obj.WatchPosition
-import com.google.common.reflect.TypeToken
-import com.google.gson.Gson
-import java.lang.reflect.Type
+import com.github.libretube.util.toID
object PreferenceHelper {
private val TAG = "PreferenceHelper"
- fun setString(context: Context, key: String?, value: String?) {
- val editor = getDefaultSharedPreferencesEditor(context)
- editor.putString(key, value)
- editor.apply()
+ private lateinit var prefContext: Context
+ private lateinit var settings: SharedPreferences
+ private lateinit var editor: SharedPreferences.Editor
+ private val mapper = ObjectMapper()
+
+ /**
+ * set the context that is being used to access the shared preferences
+ */
+ fun setContext(context: Context) {
+ prefContext = context
+ settings = getDefaultSharedPreferences(prefContext)
+ editor = settings.edit()
}
- fun setInt(context: Context, key: String?, value: Int) {
- val editor = getDefaultSharedPreferencesEditor(context)
- editor.putInt(key, value)
- editor.apply()
+ fun getString(key: String?, defValue: String?): String {
+ return settings.getString(key, defValue)!!
}
- fun setLong(context: Context, key: String?, value: Long) {
- val editor = getDefaultSharedPreferencesEditor(context)
- editor.putLong(key, value)
- editor.apply()
- }
-
- fun setBoolean(context: Context, key: String?, value: Boolean) {
- val editor = getDefaultSharedPreferencesEditor(context)
- editor.putBoolean(key, value)
- editor.apply()
- }
-
- fun getString(context: Context, key: String?, defValue: String?): String? {
- val settings: SharedPreferences = getDefaultSharedPreferences(context)
- return settings.getString(key, defValue)
- }
-
- fun getInt(context: Context, key: String?, defValue: Int): Int {
- val settings: SharedPreferences = getDefaultSharedPreferences(context)
- return settings.getInt(key, defValue)
- }
-
- fun getLong(context: Context, key: String?, defValue: Long): Long {
- val settings: SharedPreferences = getDefaultSharedPreferences(context)
- return settings.getLong(key, defValue)
- }
-
- fun getBoolean(context: Context, key: String?, defValue: Boolean): Boolean {
- val settings: SharedPreferences = getDefaultSharedPreferences(context)
+ fun getBoolean(key: String?, defValue: Boolean): Boolean {
return settings.getBoolean(key, defValue)
}
- fun clearPreferences(context: Context) {
- val editor = getDefaultSharedPreferencesEditor(context)
+ fun clearPreferences() {
editor.clear().apply()
}
- fun removePreference(context: Context, value: String?) {
- val editor = getDefaultSharedPreferencesEditor(context)
+ fun removePreference(value: String?) {
editor.remove(value).apply()
}
- fun getToken(context: Context): String {
- val sharedPref = context.getSharedPreferences("token", Context.MODE_PRIVATE)
+ fun getToken(): String {
+ val sharedPref = prefContext.getSharedPreferences("token", Context.MODE_PRIVATE)
return sharedPref?.getString("token", "")!!
}
- fun setToken(context: Context, newValue: String) {
- val editor = context.getSharedPreferences("token", Context.MODE_PRIVATE).edit()
+ fun setToken(newValue: String) {
+ val editor = prefContext.getSharedPreferences("token", Context.MODE_PRIVATE).edit()
editor.putString("token", newValue).apply()
}
- fun getUsername(context: Context): String {
- val sharedPref = context.getSharedPreferences("username", Context.MODE_PRIVATE)
+ fun getUsername(): String {
+ val sharedPref = prefContext.getSharedPreferences("username", Context.MODE_PRIVATE)
return sharedPref.getString("username", "")!!
}
- fun setUsername(context: Context, newValue: String) {
- val editor = context.getSharedPreferences("username", Context.MODE_PRIVATE).edit()
+ fun setUsername(newValue: String) {
+ val editor = prefContext.getSharedPreferences("username", Context.MODE_PRIVATE).edit()
editor.putString("username", newValue).apply()
}
- fun saveCustomInstance(context: Context, customInstance: CustomInstance) {
- val editor = getDefaultSharedPreferencesEditor(context)
- val gson = Gson()
-
- val customInstancesList = getCustomInstances(context)
+ fun saveCustomInstance(customInstance: CustomInstance) {
+ val customInstancesList = getCustomInstances()
customInstancesList += customInstance
- val json = gson.toJson(customInstancesList)
+ val json = mapper.writeValueAsString(customInstancesList)
editor.putString("customInstances", json).apply()
}
- fun getCustomInstances(context: Context): ArrayList {
- val settings = getDefaultSharedPreferences(context)
- val gson = Gson()
+ fun getCustomInstances(): ArrayList {
val json: String = settings.getString("customInstances", "")!!
- val type: Type = object : TypeToken?>() {}.type
+ val type = mapper.typeFactory.constructCollectionType(
+ List::class.java,
+ CustomInstance::class.java
+ )
return try {
- gson.fromJson(json, type)
+ mapper.readValue(json, type)
} catch (e: Exception) {
arrayListOf()
}
}
- fun getHistory(context: Context): List {
+ fun getSearchHistory(): List {
return try {
- val settings = getDefaultSharedPreferences(context)
- val set: Set = settings.getStringSet("search_history", HashSet())!!
- set.toList()
+ val json = settings.getString("search_history", "")!!
+ val type = object : TypeReference>() {}
+ return mapper.readValue(json, type)
} catch (e: Exception) {
emptyList()
}
}
- fun saveHistory(context: Context, historyList: List) {
- val editor = getDefaultSharedPreferencesEditor(context)
- val set: Set = HashSet(historyList)
- editor.putStringSet("search_history", set).apply()
+ fun saveToSearchHistory(query: String) {
+ val historyList = getSearchHistory().toMutableList()
+
+ if ((historyList.contains(query))) {
+ // remove from history list if already contained
+ historyList -= query
+ }
+
+ // append new query to history
+ historyList.add(0, query)
+
+ if (historyList.size > 10) {
+ historyList.removeAt(historyList.size - 1)
+ }
+
+ updateSearchHistory(historyList)
}
- fun addToWatchHistory(context: Context, videoId: String, streams: Streams) {
- val editor = getDefaultSharedPreferencesEditor(context)
- val gson = Gson()
+ fun removeFromSearchHistory(query: String) {
+ val historyList = getSearchHistory().toMutableList()
+ historyList -= query
+ updateSearchHistory(historyList)
+ }
+
+ private fun updateSearchHistory(historyList: List) {
+ val json = mapper.writeValueAsString(historyList)
+ editor.putString("search_history", json).apply()
+ }
+
+ fun addToWatchHistory(videoId: String, streams: Streams) {
+ removeFromWatchHistory(videoId)
val watchHistoryItem = WatchHistoryItem(
videoId,
streams.title,
streams.uploadDate,
streams.uploader,
- streams.uploaderUrl?.replace("/channel/", ""),
+ streams.uploaderUrl.toID(),
streams.uploaderAvatar,
streams.thumbnailUrl,
streams.duration
)
- val watchHistory = getWatchHistory(context)
+ val watchHistory = getWatchHistory()
+
+ watchHistory += watchHistoryItem
+
+ // remove oldest item when the watch history is longer than the pref
+ val maxWatchHistorySize = getString(PreferenceKeys.WATCH_HISTORY_SIZE, "unlimited")
+ if (maxWatchHistorySize != "unlimited" && watchHistory.size > maxWatchHistorySize.toInt()) {
+ watchHistory.removeAt(0)
+ }
+
+ val json = mapper.writeValueAsString(watchHistory)
+ editor.putString("watch_history", json).apply()
+ }
+
+ fun removeFromWatchHistory(videoId: String) {
+ val watchHistory = getWatchHistory()
- // delete entries that have the same videoId
var indexToRemove: Int? = null
watchHistory.forEachIndexed { index, item ->
if (item.videoId == videoId) indexToRemove = index
}
- if (indexToRemove != null) watchHistory.removeAt(indexToRemove!!)
-
- watchHistory += watchHistoryItem
-
- val json = gson.toJson(watchHistory)
- editor.putString("watch_history", json).apply()
+ if (indexToRemove == null) return
+ watchHistory.removeAt(indexToRemove!!)
+ val json = mapper.writeValueAsString(watchHistory)
+ editor.putString("watch_history", json).commit()
}
- fun getWatchHistory(context: Context): ArrayList {
- val settings = getDefaultSharedPreferences(context)
- val gson = Gson()
+ fun removeFromWatchHistory(position: Int) {
+ val watchHistory = getWatchHistory()
+ watchHistory.removeAt(position)
+
+ val json = mapper.writeValueAsString(watchHistory)
+ editor.putString("watch_history", json).commit()
+ }
+
+ fun getWatchHistory(): ArrayList {
val json: String = settings.getString("watch_history", "")!!
- val type: Type = object : TypeToken?>() {}.type
+ val type = mapper.typeFactory.constructCollectionType(
+ List::class.java,
+ WatchHistoryItem::class.java
+ )
+
return try {
- gson.fromJson(json, type)
+ mapper.readValue(json, type)
} catch (e: Exception) {
arrayListOf()
}
}
- fun saveWatchPosition(context: Context, videoId: String, position: Long) {
- val editor = getDefaultSharedPreferencesEditor(context)
-
- val watchPositions = getWatchPositions(context)
+ fun saveWatchPosition(videoId: String, position: Long) {
+ val watchPositions = getWatchPositions()
val watchPositionItem = WatchPosition(videoId, position)
var indexToRemove: Int? = null
@@ -184,15 +200,12 @@ object PreferenceHelper {
watchPositions += watchPositionItem
- val gson = Gson()
- val json = gson.toJson(watchPositions)
+ val json = mapper.writeValueAsString(watchPositions)
editor.putString("watch_positions", json).commit()
}
- fun removeWatchPosition(context: Context, videoId: String) {
- val editor = getDefaultSharedPreferencesEditor(context)
-
- val watchPositions = getWatchPositions(context)
+ fun removeWatchPosition(videoId: String) {
+ val watchPositions = getWatchPositions()
var indexToRemove: Int? = null
watchPositions.forEachIndexed { index, item ->
@@ -201,28 +214,56 @@ object PreferenceHelper {
if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!)
- val gson = Gson()
- val json = gson.toJson(watchPositions)
+ val json = mapper.writeValueAsString(watchPositions)
editor.putString("watch_positions", json).commit()
}
- fun getWatchPositions(context: Context): ArrayList {
- val settings = getDefaultSharedPreferences(context)
- val gson = Gson()
+ fun getWatchPositions(): ArrayList {
val json: String = settings.getString("watch_positions", "")!!
- val type: Type = object : TypeToken?>() {}.type
+ val type = mapper.typeFactory.constructCollectionType(
+ List::class.java,
+ WatchPosition::class.java
+ )
+
return try {
- gson.fromJson(json, type)
+ mapper.readValue(json, type)
} catch (e: Exception) {
arrayListOf()
}
}
+ fun setLatestVideoId(videoId: String) {
+ editor.putString(PreferenceKeys.LAST_STREAM_VIDEO_ID, videoId)
+ }
+
+ fun getLatestVideoId(): String {
+ return getString(PreferenceKeys.LAST_STREAM_VIDEO_ID, "")
+ }
+
+ fun saveErrorLog(log: String) {
+ editor.putString(PreferenceKeys.ERROR_LOG, log).commit()
+ }
+
+ fun getErrorLog(): String {
+ return getString(PreferenceKeys.ERROR_LOG, "")
+ }
+
+ fun getLocalSubscriptions(): List {
+ val json = settings.getString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, "")
+ return try {
+ val type = object : TypeReference>() {}
+ mapper.readValue(json, type)
+ } catch (e: Exception) {
+ listOf()
+ }
+ }
+
+ fun setLocalSubscriptions(channels: List) {
+ val json = mapper.writeValueAsString(channels)
+ editor.putString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, json).commit()
+ }
+
private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
-
- private fun getDefaultSharedPreferencesEditor(context: Context): SharedPreferences.Editor {
- return getDefaultSharedPreferences(context).edit()
- }
}
diff --git a/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt
new file mode 100644
index 000000000..9dd059dbe
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/preferences/PreferenceKeys.kt
@@ -0,0 +1,106 @@
+package com.github.libretube.preferences
+
+/**
+ * keys for the shared preferences
+ */
+object PreferenceKeys {
+ /**
+ * General
+ */
+ const val LANGUAGE = "language"
+ const val REGION = "region"
+ const val AUTO_ROTATION = "auto_rotation"
+ const val BREAK_REMINDER = "break_reminder"
+
+ /**
+ * Appearance
+ */
+ const val THEME_MODE = "theme_toggle"
+ const val PURE_THEME = "pure_theme"
+ const val ACCENT_COLOR = "accent_color"
+ const val GRID_COLUMNS = "grid"
+ const val DEFAULT_TAB = "default_tab"
+ const val LABEL_VISIBILITY = "label_visibility"
+ const val HIDE_TRENDING_PAGE = "hide_trending_page"
+ const val APP_ICON = "icon_change"
+
+ /**
+ * Instance
+ */
+ const val FETCH_INSTANCE = "selectInstance"
+ const val AUTH_INSTANCE = "selectAuthInstance"
+ const val AUTH_INSTANCE_TOGGLE = "auth_instance_toggle"
+ const val CUSTOM_INSTANCE = "customInstance"
+ const val CLEAR_CUSTOM_INSTANCES = "clearCustomInstances"
+ const val LOGIN_REGISTER = "login_register"
+ const val DELETE_ACCOUNT = "delete_account"
+ const val IMPORT_SUBS = "import_from_yt"
+ const val EXPORT_SUBS = "export_subs"
+
+ /**
+ * Player
+ */
+ const val AUTO_FULLSCREEN = "auto_fullscreen"
+ const val AUTO_PLAY = "autoplay"
+ const val RELATED_STREAMS = "related_streams_toggle"
+ const val PLAYBACK_SPEED = "playback_speed"
+ const val FULLSCREEN_ORIENTATION = "fullscreen_orientation"
+ const val PAUSE_ON_SCREEN_OFF = "pause_screen_off"
+ const val WATCH_POSITION_TOGGLE = "watch_position_toggle"
+ const val WATCH_HISTORY_TOGGLE = "watch_history_toggle"
+ const val SEARCH_HISTORY_TOGGLE = "search_history_toggle"
+ const val SYSTEM_CAPTION_STYLE = "system_caption_style"
+ const val CAPTION_SETTINGS = "caption_settings"
+ const val SEEK_INCREMENT = "seek_increment"
+ const val PLAYER_VIDEO_FORMAT = "player_video_format"
+ const val DEFAULT_RESOLUTION = "default_res"
+ const val BUFFERING_GOAL = "buffering_goal"
+ const val PLAYER_AUDIO_FORMAT = "player_audio_format"
+ const val PLAYER_AUDIO_QUALITY = "player_audio_quality"
+ const val DEFAULT_SUBTITLE = "default_subtitle"
+ const val SKIP_BUTTONS = "skip_buttons"
+
+ /**
+ * Background mode
+ */
+ const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
+
+ /**
+ * Download
+ */
+ const val DOWNLOAD_LOCATION = "download_location"
+ const val DOWNLOAD_FOLDER = "download_folder"
+
+ /**
+ * Notifications
+ */
+ const val NOTIFICATION_ENABLED = "notification_toggle"
+ const val CHECKING_FREQUENCY = "checking_frequency"
+ const val REQUIRED_NETWORK = "required_network"
+ const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
+
+ /**
+ * Advanced
+ */
+ const val DATA_SAVER_MODE = "data_saver_mode"
+ const val RESET_SETTINGS = "reset_settings"
+ const val CLEAR_SEARCH_HISTORY = "clear_search_history"
+ const val CLEAR_WATCH_HISTORY = "clear_watch_history"
+ const val CLEAR_WATCH_POSITIONS = "clear_watch_positions"
+ const val SHARE_WITH_TIME_CODE = "share_with_time_code"
+
+ /**
+ * History
+ */
+ const val WATCH_HISTORY_SIZE = "watch_history_size"
+
+ /**
+ * Error logs
+ */
+ const val ERROR_LOG = "error_log"
+
+ /**
+ * Data
+ */
+ const val LOCAL_SUBSCRIPTIONS = "local_subscriptions"
+}
diff --git a/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt b/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt
index 1feb8b1de..6654bc8ba 100644
--- a/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt
+++ b/app/src/main/java/com/github/libretube/preferences/SponsorBlockSettings.kt
@@ -1,11 +1,11 @@
package com.github.libretube.preferences
import android.os.Bundle
-import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
+import com.github.libretube.views.MaterialPreferenceFragment
-class SponsorBlockSettings : PreferenceFragmentCompat() {
+class SponsorBlockSettings : MaterialPreferenceFragment() {
private val TAG = "SponsorBlockSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt
new file mode 100644
index 000000000..0ad0f53b8
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt
@@ -0,0 +1,322 @@
+package com.github.libretube.services
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.widget.Toast
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.github.libretube.BACKGROUND_CHANNEL_ID
+import com.github.libretube.Globals
+import com.github.libretube.PLAYER_NOTIFICATION_ID
+import com.github.libretube.R
+import com.github.libretube.obj.Segment
+import com.github.libretube.obj.Segments
+import com.github.libretube.obj.Streams
+import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
+import com.github.libretube.util.AutoPlayHelper
+import com.github.libretube.util.NowPlayingNotification
+import com.github.libretube.util.PlayerHelper
+import com.github.libretube.util.RetrofitInstance
+import com.github.libretube.util.toID
+import com.google.android.exoplayer2.C
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.MediaItem
+import com.google.android.exoplayer2.Player
+import com.google.android.exoplayer2.audio.AudioAttributes
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Loads the selected videos audio in background mode with a notification area.
+ */
+class BackgroundMode : Service() {
+ /**
+ * VideoId of the video
+ */
+ private lateinit var videoId: String
+
+ /**
+ *PlaylistId for autoplay
+ */
+ private var playlistId: String? = null
+
+ /**
+ * The response that gets when called the Api.
+ */
+ private var streams: Streams? = null
+
+ /**
+ * The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro)
+ */
+ private var player: ExoPlayer? = null
+ private var playWhenReadyPlayer = true
+
+ /**
+ * The [AudioAttributes] handle the audio focus of the [player]
+ */
+ private lateinit var audioAttributes: AudioAttributes
+
+ /**
+ * SponsorBlock Segment data
+ */
+ private var segmentData: Segments? = null
+
+ /**
+ * [Notification] for the player
+ */
+ private lateinit var nowPlayingNotification: NowPlayingNotification
+
+ /**
+ * The [videoId] of the next stream for autoplay
+ */
+ private var nextStreamId: String? = null
+
+ /**
+ * Helper for finding the next video in the playlist
+ */
+ private lateinit var autoPlayHelper: AutoPlayHelper
+
+ /**
+ * Autoplay Preference
+ */
+ private val autoplay = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_PLAY, true)
+
+ /**
+ * Setting the required [Notification] for running as a foreground service
+ */
+ override fun onCreate() {
+ super.onCreate()
+ if (Build.VERSION.SDK_INT >= 26) {
+ val channelId = BACKGROUND_CHANNEL_ID
+ val channel = NotificationChannel(
+ channelId,
+ "Background Service",
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ val notification: Notification = Notification.Builder(this, channelId)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.playingOnBackground)).build()
+ startForeground(PLAYER_NOTIFICATION_ID, notification)
+ }
+ }
+
+ /**
+ * Initializes the [player] with the [MediaItem].
+ */
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ try {
+ // clear the playing queue
+ Globals.playingQueue.clear()
+
+ // get the intent arguments
+ videoId = intent?.getStringExtra("videoId")!!
+ playlistId = intent.getStringExtra("playlistId")
+ val position = intent.getLongExtra("position", 0L)
+
+ // initialize the playlist autoPlay Helper
+ if (playlistId != null) autoPlayHelper = AutoPlayHelper(playlistId!!)
+
+ // play the audio in the background
+ playAudio(videoId, position)
+ } catch (e: Exception) {
+ stopForeground(true)
+ stopSelf()
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ /**
+ * Gets the video data and prepares the [player].
+ */
+ private fun playAudio(
+ videoId: String,
+ seekToPosition: Long = 0
+ ) {
+ // append the video to the playing queue
+ Globals.playingQueue += videoId
+ runBlocking {
+ val job = launch {
+ streams = RetrofitInstance.api.getStreams(videoId)
+ }
+ // Wait until the job is done, to load correctly later in the player
+ job.join()
+
+ initializePlayer()
+ setMediaItem()
+
+ // create the notification
+ if (!this@BackgroundMode::nowPlayingNotification.isInitialized) {
+ nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!)
+ }
+ nowPlayingNotification.updatePlayerNotification(streams!!)
+
+ player?.apply {
+ playWhenReady = playWhenReadyPlayer
+ prepare()
+ }
+
+ // seek to the previous position if available
+ if (seekToPosition != 0L) player?.seekTo(seekToPosition)
+
+ // set the playback speed
+ val playbackSpeed = PreferenceHelper.getString(
+ PreferenceKeys.BACKGROUND_PLAYBACK_SPEED,
+ "1"
+ ).toFloat()
+ player?.setPlaybackSpeed(playbackSpeed)
+
+ fetchSponsorBlockSegments()
+
+ if (autoplay) setNextStream()
+ }
+ }
+
+ /**
+ * create the player
+ */
+ private fun initializePlayer() {
+ if (player != null) return
+
+ audioAttributes = AudioAttributes.Builder()
+ .setUsage(C.USAGE_MEDIA)
+ .setContentType(C.CONTENT_TYPE_MUSIC)
+ .build()
+ player = ExoPlayer.Builder(this)
+ .setAudioAttributes(audioAttributes, true)
+ .build()
+
+ /**
+ * Listens for changed playbackStates (e.g. pause, end)
+ * Plays the next video when the current one ended
+ */
+ player!!.addListener(object : Player.Listener {
+ override fun onPlaybackStateChanged(@Player.State state: Int) {
+ when (state) {
+ Player.STATE_ENDED -> {
+ if (autoplay) playNextVideo()
+ }
+ Player.STATE_IDLE -> {
+ onDestroy()
+ }
+ }
+ }
+ })
+ }
+
+ /**
+ * set the videoId of the next stream for autoplay
+ */
+ private fun setNextStream() {
+ if (streams!!.relatedStreams!!.isNotEmpty()) {
+ nextStreamId = streams?.relatedStreams!![0].url.toID()
+ }
+
+ if (playlistId == null) return
+ if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
+ // search for the next videoId in the playlist
+ CoroutineScope(Dispatchers.IO).launch {
+ nextStreamId = autoPlayHelper.getNextVideoId(videoId, streams!!.relatedStreams!!)
+ }
+ }
+
+ /**
+ * Plays the first related video to the current (used when the playback of the current video ended)
+ */
+ private fun playNextVideo() {
+ if (nextStreamId == null || nextStreamId == videoId) return
+ val nextQueueVideo = autoPlayHelper.getNextPlayingQueueVideoId(videoId)
+ if (nextQueueVideo != null) nextStreamId = nextQueueVideo
+
+ // play new video on background
+ this.videoId = nextStreamId!!
+ this.segmentData = null
+ playAudio(videoId)
+ }
+
+ /**
+ * Sets the [MediaItem] with the [streams] into the [player]
+ */
+ private fun setMediaItem() {
+ streams?.let {
+ val mediaItem = MediaItem.Builder().setUri(it.hls!!).build()
+ player?.setMediaItem(mediaItem)
+ }
+ }
+
+ /**
+ * fetch the segments for SponsorBlock
+ */
+ private fun fetchSponsorBlockSegments() {
+ CoroutineScope(Dispatchers.IO).launch {
+ kotlin.runCatching {
+ val categories = PlayerHelper.getSponsorBlockCategories()
+ if (categories.size > 0) {
+ segmentData =
+ RetrofitInstance.api.getSegments(
+ videoId,
+ ObjectMapper().writeValueAsString(categories)
+ )
+ checkForSegments()
+ }
+ }
+ }
+ }
+
+ /**
+ * check for SponsorBlock segments
+ */
+ private fun checkForSegments() {
+ Handler(Looper.getMainLooper()).postDelayed(this::checkForSegments, 100)
+
+ if (segmentData == null || segmentData!!.segments.isEmpty()) return
+
+ segmentData!!.segments.forEach { segment: Segment ->
+ val segmentStart = (segment.segment!![0] * 1000f).toLong()
+ val segmentEnd = (segment.segment[1] * 1000f).toLong()
+ val currentPosition = player?.currentPosition
+ if (currentPosition in segmentStart until segmentEnd) {
+ if (PreferenceHelper.getBoolean(
+ "sb_notifications_key",
+ true
+ )
+ ) {
+ try {
+ Toast.makeText(this, R.string.segment_skipped, Toast.LENGTH_SHORT)
+ .show()
+ } catch (e: Exception) {
+ // Do nothing.
+ }
+ }
+ player?.seekTo(segmentEnd)
+ }
+ }
+ }
+
+ /**
+ * destroy the [BackgroundMode] foreground service
+ */
+ override fun onDestroy() {
+ // called when the user pressed stop in the notification
+ // stop the service from being in the foreground and remove the notification
+ stopForeground(true)
+ // destroy the service
+ stopSelf()
+ if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroy()
+ super.onDestroy()
+ }
+
+ override fun onBind(p0: Intent?): IBinder? {
+ return null
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/services/ClosingService.kt b/app/src/main/java/com/github/libretube/services/ClosingService.kt
index 716e6dff9..b54debbdc 100644
--- a/app/src/main/java/com/github/libretube/services/ClosingService.kt
+++ b/app/src/main/java/com/github/libretube/services/ClosingService.kt
@@ -5,8 +5,8 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
-import android.util.Log
import androidx.annotation.Nullable
+import com.github.libretube.PLAYER_NOTIFICATION_ID
class ClosingService : Service() {
private val TAG = "ClosingService"
@@ -20,10 +20,9 @@ class ClosingService : Service() {
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
- // destroy all notifications (especially the player notification)
+ // destroy the player notification when the app gets destroyed
val nManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- nManager.cancelAll()
- Log.e(TAG, "closed")
+ nManager.cancel(PLAYER_NOTIFICATION_ID)
// Destroy the service
stopSelf()
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 63e1ffe70..a0d7c5cf3 100644
--- a/app/src/main/java/com/github/libretube/services/DownloadService.kt
+++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt
@@ -17,25 +17,26 @@ import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
-import com.arthenica.ffmpegkit.FFmpegKit
+import com.github.libretube.DOWNLOAD_CHANNEL_ID
+import com.github.libretube.DOWNLOAD_FAILURE_NOTIFICATION_ID
+import com.github.libretube.DOWNLOAD_PENDING_NOTIFICATION_ID
+import com.github.libretube.DOWNLOAD_SUCCESS_NOTIFICATION_ID
+import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.obj.DownloadType
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import java.io.File
-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 videoName: String
private lateinit var videoUrl: String
private lateinit var audioUrl: String
- private lateinit var extension: String
- private var duration: Int = 0
private var downloadType: Int = 3
private lateinit var audioDir: File
@@ -44,17 +45,15 @@ class DownloadService : Service() {
private lateinit var tempDir: File
override fun onCreate() {
super.onCreate()
- IS_DOWNLOAD_RUNNING = true
+ Globals.IS_DOWNLOAD_RUNNING = true
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- videoId = intent?.getStringExtra("videoId")!!
+ videoName = intent?.getStringExtra("videoName")!!
videoUrl = intent.getStringExtra("videoUrl")!!
audioUrl = intent.getStringExtra("audioUrl")!!
- duration = intent.getIntExtra("duration", 1)
- extension = PreferenceHelper.getString(this, "video_format", ".mp4")!!
- downloadType = if (audioUrl != "" && videoUrl != "") DownloadType.MUX
- else if (audioUrl != "") DownloadType.AUDIO
+
+ downloadType = if (audioUrl != "") DownloadType.AUDIO
else if (videoUrl != "") DownloadType.VIDEO
else DownloadType.NONE
if (downloadType != DownloadType.NONE) {
@@ -86,8 +85,8 @@ class DownloadService : Service() {
Log.e(TAG, "Directory already have")
}
- val downloadLocationPref = PreferenceHelper.getString(this, "download_location", "")
- val folderName = PreferenceHelper.getString(this, "download_folder", "LibreTube")
+ val downloadLocationPref = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_LOCATION, "")
+ val folderName = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_FOLDER, "LibreTube")
val location = when (downloadLocationPref) {
"downloads" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
@@ -111,18 +110,8 @@ class DownloadService : Service() {
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
when (downloadType) {
- DownloadType.MUX -> {
- audioDir = File(tempDir, "$videoId-audio")
- videoDir = File(tempDir, "$videoId-video")
- downloadId = downloadManagerRequest(
- getString(R.string.video),
- getString(R.string.downloading),
- videoUrl,
- videoDir
- )
- }
DownloadType.VIDEO -> {
- videoDir = File(libretubeDir, "$videoId-video")
+ videoDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest(
getString(R.string.video),
getString(R.string.downloading),
@@ -131,7 +120,7 @@ class DownloadService : Service() {
)
}
DownloadType.AUDIO -> {
- audioDir = File(libretubeDir, "$videoId-audio")
+ audioDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest(
getString(R.string.audio),
getString(R.string.downloading),
@@ -142,6 +131,7 @@ class DownloadService : Service() {
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "download error $e")
+ downloadFailedNotification()
}
}
@@ -162,8 +152,6 @@ class DownloadService : Service() {
downloadSucceededNotification()
onDestroy()
}
- } else {
- muxDownloadedMedia()
}
}
}
@@ -196,7 +184,7 @@ class DownloadService : Service() {
}
// Creating a notification and setting its various attributes
notification =
- NotificationCompat.Builder(this@DownloadService, "download_service")
+ NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle("LibreTube")
.setContentText(getString(R.string.downloading))
@@ -206,70 +194,31 @@ class DownloadService : Service() {
.setProgress(100, 0, true)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
- startForeground(2, notification.build())
+ startForeground(DOWNLOAD_PENDING_NOTIFICATION_ID, notification.build())
}
private fun downloadFailedNotification() {
- val builder = NotificationCompat.Builder(this@DownloadService, "download_service")
+ val builder = NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle(resources.getString(R.string.downloadfailed))
.setContentText(getString(R.string.fail))
.setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define
- notify(3, builder.build())
+ notify(DOWNLOAD_FAILURE_NOTIFICATION_ID, builder.build())
}
}
private fun downloadSucceededNotification() {
Log.i(TAG, "Download succeeded")
- val builder = NotificationCompat.Builder(this@DownloadService, "download_service")
+ val builder = NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle(resources.getString(R.string.success))
- .setContentText(getString(R.string.fail))
+ .setContentText(getString(R.string.downloadsucceeded))
.setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define
- notify(4, builder.build())
- }
- }
-
- private fun muxDownloadedMedia() {
- val command = "-y -i $videoDir -i $audioDir -c copy $libretubeDir/${videoId}$extension"
- notification.setContentTitle("Muxing")
- FFmpegKit.executeAsync(
- command,
- { session ->
- val state = session.state
- val returnCode = session.returnCode
- // CALLED WHEN SESSION IS EXECUTED
- Log.d(
- TAG,
- String.format(
- "FFmpeg process exited with state %s and rc %s.%s",
- state,
- returnCode,
- session.failStackTrace
- )
- )
- tempDir.deleteRecursively()
- if (returnCode.toString() != "0") downloadFailedNotification()
- else downloadSucceededNotification()
- onDestroy()
- },
- {
- // CALLED WHEN SESSION PRINTS LOGS
- Log.e(TAG, it.message.toString())
- }
- ) {
- // CALLED WHEN SESSION GENERATES STATISTICS
- Log.e(TAG + "stat", it.time.toString())
- /*val progress = it.time/(10*duration!!)
- if (progress<1){
- notification
- .setProgress(progressMax, progress.toInt(), false)
- service.notify(1,notification.build())
- }*/
+ notify(DOWNLOAD_SUCCESS_NOTIFICATION_ID, builder.build())
}
}
@@ -279,7 +228,7 @@ class DownloadService : Service() {
} catch (e: Exception) {
}
- IS_DOWNLOAD_RUNNING = false
+ Globals.IS_DOWNLOAD_RUNNING = false
Log.d(TAG, "dl finished!")
stopForeground(true)
stopService(Intent(this@DownloadService, DownloadService::class.java))
diff --git a/app/src/main/java/com/github/libretube/services/UpdateService.kt b/app/src/main/java/com/github/libretube/services/UpdateService.kt
new file mode 100644
index 000000000..5c34cf38b
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/services/UpdateService.kt
@@ -0,0 +1,89 @@
+package com.github.libretube.services
+
+import android.app.DownloadManager
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Environment
+import android.os.IBinder
+import android.widget.Toast
+import com.github.libretube.R
+import java.io.File
+
+class UpdateService : Service() {
+ private val TAG = "UpdateService"
+ private lateinit var downloadUrl: String
+ private var downloadId: Long = -1
+ private lateinit var file: File
+ private lateinit var downloadManager: DownloadManager
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ downloadUrl = intent?.getStringExtra("downloadUrl")!!
+
+ downloadApk(downloadUrl)
+
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun downloadApk(downloadUrl: String) {
+ val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ // val dir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
+ file = File(dir, "release.apk")
+
+ val request: DownloadManager.Request =
+ DownloadManager.Request(Uri.parse(downloadUrl))
+ .setTitle(getString(R.string.downloading_apk))
+ .setDescription("")
+ .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
+ .setDestinationUri(Uri.fromFile(file))
+ .setAllowedOverMetered(true)
+ .setAllowedOverRoaming(true)
+
+ downloadManager =
+ applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
+
+ downloadId = downloadManager.enqueue(request)
+
+ // listener for the download to end
+ registerReceiver(
+ onDownloadComplete,
+ IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ )
+ }
+
+ private val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
+ if (downloadId == id) {
+ // install the apk after download finished
+ val installIntent = Intent(Intent.ACTION_VIEW)
+ installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ installIntent.setDataAndType(
+ Uri.fromFile(file),
+ downloadManager.getMimeTypeForDownloadedFile(downloadId)
+ )
+ try {
+ startActivity(installIntent)
+ } catch (e: Exception) {
+ Toast.makeText(
+ context,
+ R.string.downloadsucceeded,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ unregisterReceiver(onDownloadComplete)
+ super.onDestroy()
+ }
+
+ override fun onBind(p0: Intent?): IBinder? {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/update/Asset.kt b/app/src/main/java/com/github/libretube/update/Asset.kt
new file mode 100644
index 000000000..a1e4a2c5e
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/update/Asset.kt
@@ -0,0 +1,20 @@
+package com.github.libretube.update
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class Asset(
+ val browser_download_url: String? = null,
+ val content_type: String? = null,
+ val created_at: String? = null,
+ val download_count: Int? = null,
+ val id: Int? = null,
+ val label: Any? = null,
+ val name: String? = null,
+ val node_id: String? = null,
+ val size: Int? = null,
+ val state: String? = null,
+ val updated_at: String? = null,
+ val uploader: Uploader? = null,
+ val url: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/update/Author.kt b/app/src/main/java/com/github/libretube/update/Author.kt
new file mode 100644
index 000000000..6b9ecd84d
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/update/Author.kt
@@ -0,0 +1,25 @@
+package com.github.libretube.update
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class Author(
+ val avatar_url: String? = null,
+ val events_url: String? = null,
+ val followers_url: String? = null,
+ val following_url: String? = null,
+ val gists_url: String? = null,
+ val gravatar_id: String? = null,
+ val html_url: String? = null,
+ val id: Int? = null,
+ val login: String? = null,
+ val node_id: String? = null,
+ val organizations_url: String? = null,
+ val received_events_url: String? = null,
+ val repos_url: String? = null,
+ val site_admin: Boolean? = null,
+ val starred_url: String? = null,
+ val subscriptions_url: String? = null,
+ val type: String? = null,
+ val url: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/update/Reactions.kt b/app/src/main/java/com/github/libretube/update/Reactions.kt
new file mode 100644
index 000000000..9a87fc84d
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/update/Reactions.kt
@@ -0,0 +1,15 @@
+package com.github.libretube.update
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class Reactions(
+ val confused: Int? = null,
+ val eyes: Int? = null,
+ val heart: Int? = null,
+ val hooray: Int? = null,
+ val laugh: Int? = null,
+ val rocket: Int? = null,
+ val total_count: Int? = null,
+ val url: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/update/UpdateChecker.kt b/app/src/main/java/com/github/libretube/update/UpdateChecker.kt
new file mode 100644
index 000000000..c47641120
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/update/UpdateChecker.kt
@@ -0,0 +1,36 @@
+package com.github.libretube.update
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.github.libretube.GITHUB_API_URL
+import java.net.URL
+
+object UpdateChecker {
+ fun getLatestReleaseInfo(): UpdateInfo? {
+ var versionInfo: UpdateInfo? = null
+ // run http request as thread to make it async
+ val thread = Thread {
+ // otherwise crashes without internet
+ versionInfo = getUpdateInfo()
+ try {
+ versionInfo = getUpdateInfo()
+ } catch (e: Exception) {
+ }
+ }
+ thread.start()
+ // wait for the thread to finish
+ thread.join()
+
+ // return the information about the latest version
+ return versionInfo
+ }
+
+ private fun getUpdateInfo(): UpdateInfo? {
+ // get the github API response
+ val latestVersionApiUrl = URL(GITHUB_API_URL)
+ val json = latestVersionApiUrl.readText()
+
+ // Parse and return the json data
+ val mapper = ObjectMapper()
+ return mapper.readValue(json, UpdateInfo::class.java)
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/update/UpdateInfo.kt b/app/src/main/java/com/github/libretube/update/UpdateInfo.kt
new file mode 100644
index 000000000..f72391c3e
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/update/UpdateInfo.kt
@@ -0,0 +1,27 @@
+package com.github.libretube.update
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class UpdateInfo(
+ val assets: List? = null,
+ val assets_url: String? = null,
+ val author: Author? = null,
+ val body: String? = null,
+ val created_at: String? = null,
+ val draft: Boolean? = null,
+ val html_url: String? = null,
+ val id: Int? = null,
+ val mentions_count: Int? = null,
+ val name: String? = null,
+ val node_id: String? = null,
+ val prerelease: Boolean? = null,
+ val published_at: String? = null,
+ val reactions: Reactions? = null,
+ val tag_name: String? = null,
+ val tarball_url: String? = null,
+ val target_commitish: String? = null,
+ val upload_url: String? = null,
+ val url: String? = null,
+ val zipball_url: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/update/Uploader.kt b/app/src/main/java/com/github/libretube/update/Uploader.kt
new file mode 100644
index 000000000..6c19aff19
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/update/Uploader.kt
@@ -0,0 +1,25 @@
+package com.github.libretube.update
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class Uploader(
+ val avatar_url: String? = null,
+ val events_url: String? = null,
+ val followers_url: String? = null,
+ val following_url: String? = null,
+ val gists_url: String? = null,
+ val gravatar_id: String? = null,
+ val html_url: String? = null,
+ val id: Int? = null,
+ val login: String? = null,
+ val node_id: String? = null,
+ val organizations_url: String? = null,
+ val received_events_url: String? = null,
+ val repos_url: String? = null,
+ val site_admin: Boolean? = null,
+ val starred_url: String? = null,
+ val subscriptions_url: String? = null,
+ val type: String? = null,
+ val url: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt
new file mode 100644
index 000000000..d747681ea
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt
@@ -0,0 +1,87 @@
+package com.github.libretube.util
+
+import com.github.libretube.Globals
+import com.github.libretube.obj.StreamItem
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class AutoPlayHelper(
+ private val playlistId: String?
+) {
+ private val TAG = "AutoPlayHelper"
+
+ private val playlistStreamIds = mutableListOf()
+ private var playlistNextPage: String? = null
+
+ suspend fun getNextVideoId(
+ currentVideoId: String,
+ relatedStreams: List
+ ): String? {
+ return if (Globals.playingQueue.last() != currentVideoId) {
+ val currentVideoIndex = Globals.playingQueue.indexOf(currentVideoId)
+ Globals.playingQueue[currentVideoIndex + 1]
+ } else if (playlistId == null) getNextTrendingVideoId(
+ currentVideoId,
+ relatedStreams
+ ) else getNextPlaylistVideoId(
+ currentVideoId
+ )
+ }
+
+ private fun getNextTrendingVideoId(videoId: String, relatedStreams: List): String? {
+ // don't play a video if it got played before already
+ var index = 0
+ var nextStreamId: String? = null
+ while (nextStreamId == null ||
+ (
+ Globals.playingQueue.contains(nextStreamId) &&
+ Globals.playingQueue.indexOf(videoId) > Globals.playingQueue.indexOf(
+ nextStreamId
+ )
+ )
+ ) {
+ nextStreamId = relatedStreams[index].url.toID()
+ if (index + 1 < relatedStreams.size) index += 1
+ else break
+ }
+ return nextStreamId
+ }
+
+ private suspend fun getNextPlaylistVideoId(currentVideoId: String): String? {
+ // if the playlists contain the video, then save the next video as next stream
+ if (playlistStreamIds.contains(currentVideoId)) {
+ val index = playlistStreamIds.indexOf(currentVideoId)
+ // check whether there's a next video
+ return if (index + 1 < playlistStreamIds.size) playlistStreamIds[index + 1]
+ else if (playlistNextPage == null) null
+ else getNextPlaylistVideoId(currentVideoId)
+ } else if (playlistStreamIds.isEmpty() || playlistNextPage != null) {
+ // fetch the next page of the playlist
+ return withContext(Dispatchers.IO) {
+ // fetch the playlists or its nextPage's videos
+ val playlist =
+ if (playlistNextPage == null) RetrofitInstance.authApi.getPlaylist(playlistId!!)
+ else RetrofitInstance.authApi.getPlaylistNextPage(
+ playlistId!!,
+ playlistNextPage!!
+ )
+ // save the playlist urls to the list
+ playlistStreamIds += playlist.relatedStreams!!.map { it.url.toID() }
+ // save playlistNextPage for usage if video is not contained
+ playlistNextPage = playlist.nextpage
+ return@withContext getNextPlaylistVideoId(currentVideoId)
+ }
+ }
+ // return null when no nextPage is found
+ return null
+ }
+
+ fun getNextPlayingQueueVideoId(
+ currentVideoId: String
+ ): String? {
+ return if (Globals.playingQueue.last() != currentVideoId) {
+ val currentVideoIndex = Globals.playingQueue.indexOf(currentVideoId)
+ Globals.playingQueue[currentVideoIndex + 1]
+ } else null
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/BackgroundHelper.kt b/app/src/main/java/com/github/libretube/util/BackgroundHelper.kt
new file mode 100644
index 000000000..5bb55909c
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/BackgroundHelper.kt
@@ -0,0 +1,26 @@
+package com.github.libretube.util
+
+import android.content.Context
+import android.content.Intent
+import com.github.libretube.services.BackgroundMode
+
+/**
+ * Helper for starting a new Instance of the [BackgroundMode]
+ */
+object BackgroundHelper {
+ fun playOnBackground(
+ context: Context,
+ videoId: String,
+ position: Long? = null,
+ playlistId: String? = null
+ ) {
+ // create an intent for the background mode service
+ val intent = Intent(context, BackgroundMode::class.java)
+ intent.putExtra("videoId", videoId)
+ if (playlistId != null) intent.putExtra("playlistId", playlistId)
+ if (position != null) intent.putExtra("position", position)
+
+ // start the background mode as foreground service
+ context.startForegroundService(intent)
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/BackgroundMode.kt b/app/src/main/java/com/github/libretube/util/BackgroundMode.kt
deleted file mode 100644
index 9a5e1c364..000000000
--- a/app/src/main/java/com/github/libretube/util/BackgroundMode.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-package com.github.libretube.util
-
-import android.app.NotificationManager
-import android.content.Context
-import android.support.v4.media.session.MediaSessionCompat
-import com.github.libretube.obj.Streams
-import com.github.libretube.preferences.PreferenceHelper
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.MediaItem
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.audio.AudioAttributes
-import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
-import com.google.android.exoplayer2.ui.PlayerNotificationManager
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-
-/**
- * Loads the selected video audio in background mode with a notification area.
- */
-class BackgroundMode {
- /**
- * The response that gets when called the Api.
- */
- private var response: Streams? = null
-
- /**
- * The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro)
- */
- private var player: ExoPlayer? = null
- private var playWhenReadyPlayer = true
-
- /**
- * The [MediaSessionCompat] for the [response].
- */
- private lateinit var mediaSession: MediaSessionCompat
-
- /**
- * The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
- */
- private lateinit var mediaSessionConnector: MediaSessionConnector
-
- /**
- * The [PlayerNotificationManager] to load the [mediaSession] content on it.
- */
- private var playerNotification: PlayerNotificationManager? = null
-
- /**
- * The [AudioAttributes] handle the audio focus of the [player]
- */
- private lateinit var audioAttributes: AudioAttributes
-
- /**
- * Initializes the [player] with the [MediaItem].
- */
- private fun initializePlayer(c: Context) {
- audioAttributes = AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.CONTENT_TYPE_MUSIC)
- .build()
-
- if (player == null) {
- player = ExoPlayer.Builder(c)
- .setAudioAttributes(audioAttributes, true)
- .build()
- }
-
- /**
- * Listens for changed playbackStates (e.g. pause, end)
- * Plays the next video when the current one ended
- */
- player!!.addListener(object : Player.Listener {
- override fun onPlaybackStateChanged(@Player.State state: Int) {
- val autoplay = PreferenceHelper.getBoolean(c, "autoplay", false)
- if (state == Player.STATE_ENDED) {
- if (autoplay) playNextVideo(c)
- }
- }
- })
- setMediaItem(c)
- }
-
- /**
- * Plays the first related video to the current (used when the playback of the current video ended)
- */
- private fun playNextVideo(c: Context) {
- if (response!!.relatedStreams!!.isNotEmpty()) {
- val videoId = response!!
- .relatedStreams!![0].url!!
- .replace("/watch?v=", "")
-
- // destroy old player and its notification
- playerNotification = null
- player = null
-
- // kill old notification
- val notificationManager = c.getSystemService(Context.NOTIFICATION_SERVICE)
- as NotificationManager
- notificationManager.cancel(1)
-
- // play new video on background
- playOnBackgroundMode(c, videoId)
- }
- }
-
- /**
- * Initializes the [playerNotification] attached to the [player] and shows it.
- */
- private fun initializePlayerNotification(c: Context) {
- playerNotification = PlayerNotificationManager
- .Builder(c, 1, "background_mode")
- // set the description of the notification
- .setMediaDescriptionAdapter(
- DescriptionAdapter(
- response?.title!!,
- response?.uploader!!,
- response?.thumbnailUrl!!,
- c
- )
- )
- .build()
- playerNotification?.apply {
- setPlayer(player)
- setUseNextAction(false)
- setUsePreviousAction(false)
- setUseStopAction(true)
- setColorized(true)
- setMediaSessionToken(mediaSession.sessionToken)
- }
- }
-
- /**
- * Sets the [MediaItem] with the [response] into the [player]. Also creates a [MediaSessionConnector]
- * with the [mediaSession] and attach it to the [player].
- */
- private fun setMediaItem(c: Context) {
- response?.let {
- val mediaItem = MediaItem.Builder().setUri(it.hls!!).build()
- player?.setMediaItem(mediaItem)
- }
-
- mediaSession = MediaSessionCompat(c, this.javaClass.name)
- mediaSession.isActive = true
-
- mediaSessionConnector = MediaSessionConnector(mediaSession)
- mediaSessionConnector.setPlayer(player)
- }
-
- /**
- * Gets the video data and prepares the [player].
- */
- fun playOnBackgroundMode(
- c: Context,
- videoId: String,
- seekToPosition: Long = 0
- ) {
- runBlocking {
- val job = launch {
- response = RetrofitInstance.api.getStreams(videoId)
- }
- // Wait until the job is done, to load correctly later in the player
- job.join()
-
- initializePlayer(c)
- initializePlayerNotification(c)
-
- player?.apply {
- playWhenReady = playWhenReadyPlayer
- prepare()
- }
-
- if (!seekToPosition.equals(0)) player?.seekTo(seekToPosition)
- }
- }
-
- /**
- * Creates a singleton of this class, to not create a new [player] every time.
- */
- companion object {
- private var INSTANCE: BackgroundMode? = null
-
- fun getInstance(): BackgroundMode {
- if (INSTANCE == null) INSTANCE = BackgroundMode()
- return INSTANCE!!
- }
- }
-}
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 1ba230baa..6f1e93bb3 100644
--- a/app/src/main/java/com/github/libretube/util/ConnectionHelper.kt
+++ b/app/src/main/java/com/github/libretube/util/ConnectionHelper.kt
@@ -2,6 +2,10 @@ package com.github.libretube.util
import android.content.Context
import android.net.ConnectivityManager
+import android.widget.ImageView
+import coil.ImageLoader
+import coil.load
+import com.github.libretube.Globals
object ConnectionHelper {
fun isNetworkAvailable(context: Context): Boolean {
@@ -33,4 +37,14 @@ object ConnectionHelper {
return connectivityManager.activeNetworkInfo?.isConnected ?: false
}
+
+ lateinit var imageLoader: ImageLoader
+
+ // load an image from a url into an imageView
+ fun loadImage(url: String?, target: ImageView) {
+ // only load the image if the data saver mode is disabled
+ if (!Globals.DATA_SAVER_MODE_ENABLED) {
+ target.load(url, imageLoader)
+ }
+ }
}
diff --git a/app/src/main/java/com/github/libretube/util/CronetHelper.kt b/app/src/main/java/com/github/libretube/util/CronetHelper.kt
index 61bb62e75..cc50a9fa9 100644
--- a/app/src/main/java/com/github/libretube/util/CronetHelper.kt
+++ b/app/src/main/java/com/github/libretube/util/CronetHelper.kt
@@ -1,16 +1,20 @@
package com.github.libretube.util
import android.content.Context
+import com.google.net.cronet.okhttptransport.CronetCallFactory
import org.chromium.net.CronetEngine
class CronetHelper {
companion object {
private lateinit var engine: CronetEngine
+ lateinit var callFactory: CronetCallFactory
fun initCronet(context: Context) {
this.engine = CronetEngine.Builder(context)
.enableBrotli(true)
.build()
+ callFactory = CronetCallFactory.newBuilder(this.engine)
+ .build()
}
fun getCronetEngine(): CronetEngine {
diff --git a/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt b/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt
deleted file mode 100644
index 6bc27bf40..000000000
--- a/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.github.libretube.util
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import com.github.libretube.activities.MainActivity
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.ui.PlayerNotificationManager
-import java.net.URL
-
-/**
- * The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
- * Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
- */
-class DescriptionAdapter(
- private val title: String,
- private val channelName: String,
- private val thumbnailUrl: String,
- private val context: Context
-) :
- PlayerNotificationManager.MediaDescriptionAdapter {
- /**
- * sets the title of the notification
- */
- override fun getCurrentContentTitle(player: Player): CharSequence {
- // return controller.metadata.description.title.toString()
- return title
- }
-
- /**
- * overrides the action when clicking the notification
- */
- override fun createCurrentContentIntent(player: Player): PendingIntent? {
- // return controller.sessionActivity
- /**
- * starts a new MainActivity Intent when the player notification is clicked
- * it doesn't start a completely new MainActivity because the MainActivity's launchMode
- * is set to "singleTop" in the AndroidManifest (important!!!)
- * that's the only way to launch back into the previous activity (e.g. the player view
- */
- val intent = Intent(context, MainActivity::class.java)
- return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
- }
-
- /**
- * the description of the notification (below the title)
- */
- override fun getCurrentContentText(player: Player): CharSequence? {
- // return controller.metadata.description.subtitle.toString()
- return channelName
- }
-
- /**
- * return the icon/thumbnail of the video
- */
- override fun getCurrentLargeIcon(
- player: Player,
- callback: PlayerNotificationManager.BitmapCallback
- ): Bitmap? {
- lateinit var bitmap: Bitmap
-
- /**
- * running on a new thread to prevent a NetworkMainThreadException
- */
- val thread = Thread {
- try {
- /**
- * try to GET the thumbnail from the URL
- */
- val inputStream = URL(thumbnailUrl).openStream()
- bitmap = BitmapFactory.decodeStream(inputStream)
- } catch (ex: java.lang.Exception) {
- ex.printStackTrace()
- }
- }
- thread.start()
- thread.join()
- /**
- * returns the scaled bitmap if it got fetched successfully
- */
- return try {
- val resizedBitmap = Bitmap.createScaledBitmap(
- bitmap,
- bitmap.width,
- bitmap.width,
- false
- )
- resizedBitmap
- } catch (e: Exception) {
- null
- }
- }
-}
diff --git a/app/src/main/java/com/github/libretube/util/DoubleTapListener.kt b/app/src/main/java/com/github/libretube/util/DoubleTapListener.kt
new file mode 100644
index 000000000..4374b9597
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/DoubleTapListener.kt
@@ -0,0 +1,45 @@
+package com.github.libretube.util
+
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.view.View
+
+abstract class DoubleTapListener : View.OnClickListener {
+
+ private var isSingleEvent = false
+ private val doubleClickQualificationSpanInMillis: Long
+ private var timestampLastClick: Long
+ private val handler: Handler
+ private val runnable: Runnable
+
+ override fun onClick(v: View?) {
+ if (SystemClock.elapsedRealtime() - timestampLastClick < doubleClickQualificationSpanInMillis) {
+ isSingleEvent = false
+ handler.removeCallbacks(runnable)
+ onDoubleClick()
+ return
+ }
+ isSingleEvent = true
+ handler.postDelayed(runnable, DEFAULT_QUALIFICATION_SPAN)
+ timestampLastClick = SystemClock.elapsedRealtime()
+ }
+
+ abstract fun onDoubleClick()
+ abstract fun onSingleClick()
+
+ companion object {
+ private const val DEFAULT_QUALIFICATION_SPAN: Long = 200
+ }
+
+ init {
+ doubleClickQualificationSpanInMillis = DEFAULT_QUALIFICATION_SPAN
+ timestampLastClick = 0
+ handler = Handler(Looper.getMainLooper())
+ runnable = Runnable {
+ if (isSingleEvent) {
+ onSingleClick()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/ExceptionHandler.kt b/app/src/main/java/com/github/libretube/util/ExceptionHandler.kt
new file mode 100644
index 000000000..af8185c84
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/ExceptionHandler.kt
@@ -0,0 +1,14 @@
+package com.github.libretube.util
+
+import com.github.libretube.preferences.PreferenceHelper
+
+class ExceptionHandler(
+ private val defaultExceptionHandler: Thread.UncaughtExceptionHandler?
+) : Thread.UncaughtExceptionHandler {
+ override fun uncaughtException(thread: Thread, exc: Throwable) {
+ // save the error log
+ PreferenceHelper.saveErrorLog(exc.stackTraceToString())
+ // throw the exception with the default exception handler to make the app crash
+ defaultExceptionHandler?.uncaughtException(thread, exc)
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/ImportHelper.kt b/app/src/main/java/com/github/libretube/util/ImportHelper.kt
new file mode 100644
index 000000000..2ca3852a5
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/ImportHelper.kt
@@ -0,0 +1,129 @@
+package com.github.libretube.util
+
+import android.app.Activity
+import android.net.Uri
+import android.util.Log
+import android.widget.Toast
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.github.libretube.R
+import com.github.libretube.obj.NewPipeSubscription
+import com.github.libretube.obj.NewPipeSubscriptions
+import com.github.libretube.preferences.PreferenceHelper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import java.io.BufferedReader
+import java.io.FileOutputStream
+import java.io.InputStreamReader
+
+class ImportHelper(
+ private val activity: Activity
+) {
+ private val TAG = "ImportHelper"
+
+ /**
+ * Import subscriptions by a file uri
+ */
+ fun importSubscriptions(uri: Uri?) {
+ if (uri == null) return
+ try {
+ var channels = ArrayList()
+ val fileType = activity.contentResolver.getType(uri)
+
+ if (fileType == "application/json") {
+ // NewPipe subscriptions format
+ val mapper = ObjectMapper()
+ val json = readRawTextFromUri(uri)
+
+ val subscriptions = mapper.readValue(json, NewPipeSubscriptions::class.java)
+ channels = subscriptions.subscriptions?.map {
+ it.url?.replace("https://www.youtube.com/channel/", "")!!
+ } as ArrayList
+ } else if (
+ fileType == "text/csv" ||
+ fileType == "text/comma-separated-values"
+ ) {
+ // import subscriptions from Google/YouTube Takeout
+ val inputStream = activity.contentResolver.openInputStream(uri)
+ BufferedReader(InputStreamReader(inputStream)).use { reader ->
+ var line: String? = reader.readLine()
+ while (line != null) {
+ val channelId = line.substringBefore(",")
+ if (channelId.length == 24) channels.add(channelId)
+ line = reader.readLine()
+ }
+ }
+ inputStream?.close()
+ } else {
+ throw IllegalArgumentException("Unsupported file type")
+ }
+
+ CoroutineScope(Dispatchers.IO).launch {
+ SubscriptionHelper.importSubscriptions(channels)
+ }
+
+ Toast.makeText(activity, R.string.importsuccess, Toast.LENGTH_SHORT).show()
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ Toast.makeText(
+ activity,
+ R.string.error,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun readRawTextFromUri(uri: Uri): String {
+ val stringBuilder = StringBuilder()
+ activity.contentResolver.openInputStream(uri)?.use { inputStream ->
+ BufferedReader(InputStreamReader(inputStream)).use { reader ->
+ var line: String? = reader.readLine()
+ while (line != null) {
+ stringBuilder.append(line)
+ line = reader.readLine()
+ }
+ }
+ }
+ return stringBuilder.toString()
+ }
+
+ /**
+ * write the text to the document
+ */
+ fun exportSubscriptions(uri: Uri?) {
+ if (uri == null) return
+ try {
+ val mapper = ObjectMapper()
+ val token = PreferenceHelper.getToken()
+ runBlocking {
+ val subs = if (token != "") RetrofitInstance.authApi.subscriptions(token)
+ else RetrofitInstance.authApi.unauthenticatedSubscriptions(
+ SubscriptionHelper.getFormattedLocalSubscriptions()
+ )
+ val newPipeChannels = mutableListOf()
+ subs.forEach {
+ newPipeChannels += NewPipeSubscription(
+ name = it.name,
+ service_id = 0,
+ url = "https://www.youtube.com" + it.url
+ )
+ }
+
+ val newPipeSubscriptions = NewPipeSubscriptions(
+ subscriptions = newPipeChannels
+ )
+
+ val data = mapper.writeValueAsBytes(newPipeSubscriptions)
+
+ activity.contentResolver.openFileDescriptor(uri, "w")?.use {
+ FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
+ fileOutputStream.write(data)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/LocaleHelper.kt b/app/src/main/java/com/github/libretube/util/LocaleHelper.kt
index 8c087a1e5..a638c37b0 100644
--- a/app/src/main/java/com/github/libretube/util/LocaleHelper.kt
+++ b/app/src/main/java/com/github/libretube/util/LocaleHelper.kt
@@ -4,22 +4,24 @@ import android.content.Context
import android.os.Build
import android.telephony.TelephonyManager
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import java.util.*
object LocaleHelper {
fun updateLanguage(context: Context) {
- val languageName = PreferenceHelper.getString(context, "language", "sys")
+ val languageName = PreferenceHelper.getString(PreferenceKeys.LANGUAGE, "sys")
if (languageName == "sys") updateLocaleConf(context, Locale.getDefault())
- else if ("$languageName".length < 3) {
- val locale = Locale(languageName.toString())
- updateLocaleConf(context, locale)
- } else if ("$languageName".length > 3) {
+ else if (languageName?.contains("-") == true) {
+ val languageParts = languageName.split("-")
val locale = Locale(
- languageName?.substring(0, 2).toString(),
- languageName?.substring(4, 6).toString()
+ languageParts[0],
+ languageParts[1]
)
updateLocaleConf(context, locale)
+ } else {
+ val locale = Locale(languageName.toString())
+ updateLocaleConf(context, locale)
}
}
diff --git a/app/src/main/java/com/github/libretube/util/NavigationHelper.kt b/app/src/main/java/com/github/libretube/util/NavigationHelper.kt
new file mode 100644
index 000000000..219aa3dac
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/NavigationHelper.kt
@@ -0,0 +1,68 @@
+package com.github.libretube.util
+
+import android.content.Context
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.constraintlayout.motion.widget.MotionLayout
+import androidx.core.os.bundleOf
+import com.github.libretube.R
+import com.github.libretube.activities.MainActivity
+import com.github.libretube.fragments.PlayerFragment
+
+object NavigationHelper {
+ fun navigateChannel(
+ context: Context,
+ channelId: String?
+ ) {
+ if (channelId != null) {
+ val activity = context as MainActivity
+ val bundle = bundleOf("channel_id" to channelId)
+ activity.navController.navigate(R.id.channelFragment, bundle)
+ try {
+ val mainMotionLayout =
+ activity.findViewById(R.id.mainMotionLayout)
+ if (mainMotionLayout.progress == 0.toFloat()) {
+ mainMotionLayout.transitionToEnd()
+ activity.findViewById(R.id.playerMotionLayout)
+ .transitionToEnd()
+ }
+ } catch (e: Exception) {
+ }
+ }
+ }
+
+ fun navigateVideo(
+ context: Context,
+ videoId: String?,
+ playlistId: String? = null
+ ) {
+ if (videoId != null) {
+ val bundle = Bundle()
+ bundle.putString("videoId", videoId.toID())
+ if (playlistId != null) bundle.putString("playlistId", playlistId)
+ val frag = PlayerFragment()
+ frag.arguments = bundle
+ val activity = context as AppCompatActivity
+ activity.supportFragmentManager.beginTransaction()
+ .remove(PlayerFragment())
+ .commit()
+ activity.supportFragmentManager.beginTransaction()
+ .replace(R.id.container, frag)
+ .commitNow()
+ }
+ }
+
+ fun navigatePlaylist(
+ context: Context,
+ playlistId: String?,
+ isOwner: Boolean
+ ) {
+ if (playlistId != null) {
+ val activity = context as MainActivity
+ val bundle = Bundle()
+ bundle.putString("playlist_id", playlistId)
+ bundle.putBoolean("isOwner", isOwner)
+ activity.navController.navigate(R.id.playlistFragment, bundle)
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/NotificationHelper.kt b/app/src/main/java/com/github/libretube/util/NotificationHelper.kt
new file mode 100644
index 000000000..e649ed93d
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/NotificationHelper.kt
@@ -0,0 +1,175 @@
+package com.github.libretube.util
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import com.github.libretube.PUSH_CHANNEL_ID
+import com.github.libretube.PUSH_NOTIFICATION_ID
+import com.github.libretube.R
+import com.github.libretube.activities.MainActivity
+import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import java.util.concurrent.TimeUnit
+
+object NotificationHelper {
+ fun enqueueWork(
+ context: Context,
+ existingPeriodicWorkPolicy: ExistingPeriodicWorkPolicy
+ ) {
+ // get the notification preferences
+ PreferenceHelper.setContext(context)
+ val notificationsEnabled = PreferenceHelper.getBoolean(
+ PreferenceKeys.NOTIFICATION_ENABLED,
+ true
+ )
+
+ val checkingFrequency = PreferenceHelper.getString(
+ PreferenceKeys.CHECKING_FREQUENCY,
+ "60"
+ ).toLong()
+
+ val uniqueWorkName = "NotificationService"
+
+ // schedule the work manager request if logged in and notifications enabled
+ if (notificationsEnabled && PreferenceHelper.getToken() != "") {
+ // required network type for the work
+ val networkType = when (
+ PreferenceHelper.getString(PreferenceKeys.REQUIRED_NETWORK, "all")
+ ) {
+ "all" -> NetworkType.CONNECTED
+ "wifi" -> NetworkType.UNMETERED
+ "metered" -> NetworkType.METERED
+ else -> NetworkType.CONNECTED
+ }
+
+ // requirements for the work
+ // here: network needed to run the task
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(networkType)
+ .build()
+
+ // create the worker
+ val notificationWorker = PeriodicWorkRequest.Builder(
+ NotificationWorker::class.java,
+ checkingFrequency,
+ TimeUnit.MINUTES
+ )
+ .setConstraints(constraints)
+ .build()
+
+ // enqueue the task
+ WorkManager.getInstance(context)
+ .enqueueUniquePeriodicWork(
+ uniqueWorkName,
+ existingPeriodicWorkPolicy,
+ notificationWorker
+ )
+ } else {
+ // cancel the work if notifications are disabled or the user is not logged in
+ WorkManager.getInstance(context)
+ .cancelUniqueWork(uniqueWorkName)
+ }
+ }
+
+ /**
+ * check whether new streams are available in subscriptions
+ */
+ fun checkForNewStreams(context: Context): Boolean {
+ var result = true
+
+ val token = PreferenceHelper.getToken()
+ runBlocking {
+ val task = async {
+ if (token != "") RetrofitInstance.authApi.getFeed(token)
+ else RetrofitInstance.authApi.getUnauthenticatedFeed(
+ SubscriptionHelper.getFormattedLocalSubscriptions()
+ )
+ }
+ // fetch the users feed
+ val videoFeed = try {
+ task.await()
+ } catch (e: Exception) {
+ result = false
+ return@runBlocking
+ }
+
+ val lastSeenStreamId = PreferenceHelper.getLatestVideoId()
+ val latestFeedStreamId = videoFeed[0].url.toID()
+
+ // first time notifications enabled
+ if (lastSeenStreamId == "") PreferenceHelper.setLatestVideoId(lastSeenStreamId)
+ else if (lastSeenStreamId != latestFeedStreamId) {
+ // get the index of the last user-seen video
+ var newStreamIndex = -1
+ videoFeed.forEachIndexed { index, stream ->
+ if (stream.url?.toID() == lastSeenStreamId) {
+ newStreamIndex = index
+ }
+ }
+ if (newStreamIndex == -1) return@runBlocking
+ val (title, description) = when (newStreamIndex) {
+ // only one new stream available
+ 1 -> {
+ Pair(videoFeed[0].title, videoFeed[0].uploaderName)
+ }
+ else -> {
+ Pair(
+ // return the amount of new streams as title
+ context.getString(
+ R.string.new_streams_count,
+ newStreamIndex.toString()
+ ),
+ // return the first few uploader as description
+ context.getString(
+ R.string.new_streams_by,
+ videoFeed[0].uploaderName + ", " + videoFeed[1].uploaderName + ", " + videoFeed[2].uploaderName
+ )
+ )
+ }
+ }
+ // save the id of the last recent video for the next time it's running
+ PreferenceHelper.setLatestVideoId(videoFeed[0].url.toID())
+ createNotification(context, title!!, description!!)
+ }
+ }
+ // return whether the work succeeded
+ return result
+ }
+
+ /**
+ * Notification that is created when new streams are found
+ */
+ fun createNotification(context: Context, title: String, description: String) {
+ val intent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, PUSH_CHANNEL_ID)
+ .setContentTitle(title)
+ .setSmallIcon(R.drawable.ic_bell)
+ .setContentText(description)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ // Set the intent that will fire when the user taps the notification
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ with(NotificationManagerCompat.from(context)) {
+ // notificationId is a unique int for each notification that you must define
+ notify(PUSH_NOTIFICATION_ID, builder.build())
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/NotificationWorker.kt b/app/src/main/java/com/github/libretube/util/NotificationWorker.kt
new file mode 100644
index 000000000..027867b1b
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/NotificationWorker.kt
@@ -0,0 +1,20 @@
+package com.github.libretube.util
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+
+/**
+ * The notification worker which checks for new streams in a certain frequency
+ */
+class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
+ Worker(appContext, parameters) {
+ private val TAG = "NotificationWorker"
+
+ override fun doWork(): Result {
+ // check whether there are new streams and notify if there are some
+ val result = NotificationHelper.checkForNewStreams(applicationContext)
+ // return success if the API request succeeded
+ return if (result) Result.success() else Result.retry()
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt
new file mode 100644
index 000000000..f0f52809c
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt
@@ -0,0 +1,180 @@
+package com.github.libretube.util
+
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.support.v4.media.session.MediaSessionCompat
+import com.github.libretube.BACKGROUND_CHANNEL_ID
+import com.github.libretube.PLAYER_NOTIFICATION_ID
+import com.github.libretube.activities.MainActivity
+import com.github.libretube.obj.Streams
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.Player
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
+import com.google.android.exoplayer2.ui.PlayerNotificationManager
+import java.net.URL
+
+class NowPlayingNotification(
+ private val context: Context,
+ private val player: ExoPlayer
+) {
+ private var streams: Streams? = null
+
+ /**
+ * The [MediaSessionCompat] for the [streams].
+ */
+ private lateinit var mediaSession: MediaSessionCompat
+
+ /**
+ * The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
+ */
+ private lateinit var mediaSessionConnector: MediaSessionConnector
+
+ /**
+ * The [PlayerNotificationManager] to load the [mediaSession] content on it.
+ */
+ private var playerNotification: PlayerNotificationManager? = null
+
+ /**
+ * The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
+ * Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
+ */
+ inner class DescriptionAdapter() :
+ PlayerNotificationManager.MediaDescriptionAdapter {
+ /**
+ * sets the title of the notification
+ */
+ override fun getCurrentContentTitle(player: Player): CharSequence {
+ // return controller.metadata.description.title.toString()
+ return streams?.title!!
+ }
+
+ /**
+ * overrides the action when clicking the notification
+ */
+ override fun createCurrentContentIntent(player: Player): PendingIntent? {
+ // return controller.sessionActivity
+ /**
+ * starts a new MainActivity Intent when the player notification is clicked
+ * it doesn't start a completely new MainActivity because the MainActivity's launchMode
+ * is set to "singleTop" in the AndroidManifest (important!!!)
+ * that's the only way to launch back into the previous activity (e.g. the player view
+ */
+ val intent = Intent(context, MainActivity::class.java)
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ /**
+ * the description of the notification (below the title)
+ */
+ override fun getCurrentContentText(player: Player): CharSequence? {
+ // return controller.metadata.description.subtitle.toString()
+ return streams?.uploader
+ }
+
+ /**
+ * return the icon/thumbnail of the video
+ */
+ override fun getCurrentLargeIcon(
+ player: Player,
+ callback: PlayerNotificationManager.BitmapCallback
+ ): Bitmap? {
+ lateinit var bitmap: Bitmap
+
+ /**
+ * running on a new thread to prevent a NetworkMainThreadException
+ */
+ val thread = Thread {
+ try {
+ /**
+ * try to GET the thumbnail from the URL
+ */
+ val inputStream = URL(streams?.thumbnailUrl).openStream()
+ bitmap = BitmapFactory.decodeStream(inputStream)
+ } catch (ex: java.lang.Exception) {
+ ex.printStackTrace()
+ }
+ }
+ thread.start()
+ thread.join()
+ /**
+ * returns the scaled bitmap if it got fetched successfully
+ */
+ return try {
+ val resizedBitmap = Bitmap.createScaledBitmap(
+ bitmap,
+ bitmap.width,
+ bitmap.width,
+ false
+ )
+ resizedBitmap
+ } catch (e: Exception) {
+ null
+ }
+ }
+ }
+
+ /**
+ * Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player
+ */
+ private fun createMediaSession() {
+ if (this::mediaSession.isInitialized) return
+ mediaSession = MediaSessionCompat(context, this.javaClass.name)
+ mediaSession.isActive = true
+
+ mediaSessionConnector = MediaSessionConnector(mediaSession)
+ mediaSessionConnector.setPlayer(player)
+ }
+
+ /**
+ * Updates or creates the [playerNotification]
+ */
+ fun updatePlayerNotification(
+ streams: Streams
+ ) {
+ this.streams = streams
+ if (playerNotification == null) {
+ createMediaSession()
+ createNotification()
+ }
+ }
+
+ /**
+ * Initializes the [playerNotification] attached to the [player] and shows it.
+ */
+ private fun createNotification() {
+ playerNotification = PlayerNotificationManager
+ .Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
+ // set the description of the notification
+ .setMediaDescriptionAdapter(
+ DescriptionAdapter()
+ )
+ .build()
+ playerNotification?.apply {
+ setPlayer(player)
+ setUseNextAction(false)
+ setUsePreviousAction(false)
+ setUseStopAction(true)
+ setColorized(true)
+ setMediaSessionToken(mediaSession.sessionToken)
+ }
+ }
+
+ /**
+ * Destroy the [NowPlayingNotification]
+ */
+ fun destroy() {
+ mediaSession.isActive = false
+ mediaSession.release()
+ mediaSessionConnector.setPlayer(null)
+ playerNotification?.setPlayer(null)
+ val notificationManager = context.getSystemService(
+ Context.NOTIFICATION_SERVICE
+ ) as NotificationManager
+ notificationManager.cancel(PLAYER_NOTIFICATION_ID)
+ player.release()
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/PermissionHelper.kt b/app/src/main/java/com/github/libretube/util/PermissionHelper.kt
new file mode 100644
index 000000000..3670a2d81
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/PermissionHelper.kt
@@ -0,0 +1,68 @@
+package com.github.libretube.util
+
+import android.Manifest
+import android.app.Activity
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Environment
+import androidx.core.app.ActivityCompat
+
+object PermissionHelper {
+ fun requestReadWrite(activity: Activity): Boolean {
+ // request storage permissions if not granted yet
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ if (!Environment.isExternalStorageManager()) {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.MANAGE_EXTERNAL_STORAGE
+ ),
+ 1
+ ) // permission request code is just an int
+ return false
+ }
+ } else {
+ if (ActivityCompat.checkSelfPermission(
+ activity,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ ) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(
+ activity,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ),
+ 1
+ )
+ return false
+ }
+ }
+ return true
+ }
+
+ fun isStoragePermissionGranted(activity: Activity): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED
+ ) {
+ true
+ } else {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
+ 1
+ )
+ false
+ }
+ } else {
+ // permission is automatically granted on sdk < 23 upon installation
+ true
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/PipedApi.kt b/app/src/main/java/com/github/libretube/util/PipedApi.kt
index ca0f791a5..36fc4902b 100644
--- a/app/src/main/java/com/github/libretube/util/PipedApi.kt
+++ b/app/src/main/java/com/github/libretube/util/PipedApi.kt
@@ -66,6 +66,9 @@ interface PipedApi {
@GET("channel/{channelId}")
suspend fun getChannel(@Path("channelId") channelId: String): Channel
+ @GET("user/{name}")
+ suspend fun getChannelByName(@Path("name") channelName: String): Channel
+
@GET("nextpage/channel/{channelId}")
suspend fun getChannelNextPage(
@Path("channelId") channelId: String,
@@ -96,6 +99,9 @@ interface PipedApi {
@GET("feed")
suspend fun getFeed(@Query("authToken") token: String?): List
+ @GET("feed/unauthenticated")
+ suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List
+
@GET("subscribed")
suspend fun isSubscribed(
@Query("channelId") channelId: String,
@@ -105,6 +111,9 @@ interface PipedApi {
@GET("subscriptions")
suspend fun subscriptions(@Header("Authorization") token: String): List
+ @GET("subscriptions/unauthenticated")
+ suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List
+
@POST("subscribe")
suspend fun subscribe(
@Header("Authorization") token: String,
diff --git a/app/src/main/java/com/github/libretube/util/PlayerHelper.kt b/app/src/main/java/com/github/libretube/util/PlayerHelper.kt
new file mode 100644
index 000000000..81fcccae5
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/PlayerHelper.kt
@@ -0,0 +1,135 @@
+package com.github.libretube.util
+
+import android.content.Context
+import android.view.accessibility.CaptioningManager
+import com.github.libretube.obj.PipedStream
+import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
+import com.google.android.exoplayer2.ui.CaptionStyleCompat
+
+object PlayerHelper {
+ private val TAG = "PlayerHelper"
+
+ // get the audio source following the users preferences
+ fun getAudioSource(audios: List): String {
+ val audioFormat = PreferenceHelper.getString(PreferenceKeys.PLAYER_AUDIO_FORMAT, "all")
+ val audioQuality = PreferenceHelper.getString(PreferenceKeys.PLAYER_AUDIO_QUALITY, "best")
+
+ val mutableAudios = audios.toMutableList()
+ if (audioFormat != "all") {
+ audios.forEach {
+ val audioMimeType = "audio/$audioFormat"
+ if (it.mimeType != audioMimeType) mutableAudios.remove(it)
+ }
+ }
+
+ return if (audioQuality == "worst") {
+ getLeastBitRate(mutableAudios)
+ } else {
+ getMostBitRate(mutableAudios)
+ }
+ }
+
+ // get the best bit rate from audio streams
+ private fun getMostBitRate(audios: List): String {
+ var bitrate = 0
+ var audioUrl = ""
+ audios.forEach {
+ if (it.bitrate != null && it.bitrate!! > bitrate) {
+ bitrate = it.bitrate!!
+ audioUrl = it.url.toString()
+ }
+ }
+ return audioUrl
+ }
+
+ // get the best bit rate from audio streams
+ private fun getLeastBitRate(audios: List): String {
+ var bitrate = 1000000000
+ var audioUrl = ""
+ audios.forEach {
+ if (it.bitrate != null && it.bitrate!! < bitrate) {
+ bitrate = it.bitrate!!
+ audioUrl = it.url.toString()
+ }
+ }
+ return audioUrl
+ }
+
+ // get the system default caption style
+ fun getCaptionStyle(context: Context): CaptionStyleCompat {
+ val captioningManager =
+ context.getSystemService(Context.CAPTIONING_SERVICE) as CaptioningManager
+ return if (!captioningManager.isEnabled) {
+ // system captions are disabled, using android default captions style
+ CaptionStyleCompat.DEFAULT
+ } else {
+ // system captions are enabled
+ CaptionStyleCompat.createFromCaptionStyle(captioningManager.userStyle)
+ }
+ }
+
+ /**
+ * get the categories for sponsorBlock
+ */
+ fun getSponsorBlockCategories(): ArrayList {
+ val categories: ArrayList = arrayListOf()
+ if (PreferenceHelper.getBoolean(
+ "intro_category_key",
+ false
+ )
+ ) {
+ categories.add("intro")
+ }
+ if (PreferenceHelper.getBoolean(
+ "selfpromo_category_key",
+ false
+ )
+ ) {
+ categories.add("selfpromo")
+ }
+ if (PreferenceHelper.getBoolean(
+ "interaction_category_key",
+ false
+ )
+ ) {
+ categories.add("interaction")
+ }
+ if (PreferenceHelper.getBoolean(
+ "sponsors_category_key",
+ true
+ )
+ ) {
+ categories.add("sponsor")
+ }
+ if (PreferenceHelper.getBoolean(
+ "outro_category_key",
+ false
+ )
+ ) {
+ categories.add("outro")
+ }
+ if (PreferenceHelper.getBoolean(
+ "filler_category_key",
+ false
+ )
+ ) {
+ categories.add("filler")
+ }
+ if (PreferenceHelper.getBoolean(
+ "music_offtopic_category_key",
+ false
+ )
+ ) {
+ categories.add("music_offtopic")
+ }
+ if (PreferenceHelper.getBoolean(
+ "preview_category_key",
+ false
+ )
+ ) {
+ categories.add("preview")
+ }
+ return categories
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/util/RetrofitInstance.kt b/app/src/main/java/com/github/libretube/util/RetrofitInstance.kt
index 87caad710..c8a9eaeb7 100644
--- a/app/src/main/java/com/github/libretube/util/RetrofitInstance.kt
+++ b/app/src/main/java/com/github/libretube/util/RetrofitInstance.kt
@@ -10,6 +10,7 @@ object RetrofitInstance {
val api: PipedApi by resettableLazy(lazyMgr) {
Retrofit.Builder()
.baseUrl(url)
+ .callFactory(CronetHelper.callFactory)
.addConverterFactory(JacksonConverterFactory.create())
.build()
.create(PipedApi::class.java)
@@ -17,6 +18,7 @@ object RetrofitInstance {
val authApi: PipedApi by resettableLazy(lazyMgr) {
Retrofit.Builder()
.baseUrl(authUrl)
+ .callFactory(CronetHelper.callFactory)
.addConverterFactory(JacksonConverterFactory.create())
.build()
.create(PipedApi::class.java)
diff --git a/app/src/main/java/com/github/libretube/util/SubscriptionHelper.kt b/app/src/main/java/com/github/libretube/util/SubscriptionHelper.kt
new file mode 100644
index 000000000..c917d0900
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/util/SubscriptionHelper.kt
@@ -0,0 +1,92 @@
+package com.github.libretube.util
+
+import android.util.Log
+import com.github.libretube.obj.Subscribe
+import com.github.libretube.preferences.PreferenceHelper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+object SubscriptionHelper {
+ val TAG = "SubscriptionHelper"
+
+ fun subscribe(channelId: String) {
+ if (PreferenceHelper.getToken() != "") {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ RetrofitInstance.authApi.subscribe(
+ PreferenceHelper.getToken(),
+ Subscribe(channelId)
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ }
+ } else {
+ val channels = PreferenceHelper.getLocalSubscriptions().toMutableList()
+ channels.add(channelId)
+ PreferenceHelper.setLocalSubscriptions(channels)
+ }
+ }
+
+ fun unsubscribe(channelId: String) {
+ if (PreferenceHelper.getToken() != "") {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ RetrofitInstance.authApi.unsubscribe(
+ PreferenceHelper.getToken(),
+ Subscribe(channelId)
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ }
+ } else {
+ val channels = PreferenceHelper.getLocalSubscriptions().toMutableList()
+ channels.remove(channelId)
+ PreferenceHelper.setLocalSubscriptions(channels)
+ }
+ }
+
+ suspend fun isSubscribed(channelId: String): Boolean? {
+ if (PreferenceHelper.getToken() != "") {
+ val isSubscribed = try {
+ RetrofitInstance.authApi.isSubscribed(
+ channelId,
+ PreferenceHelper.getToken()
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ return null
+ }
+ return isSubscribed.subscribed
+ } else {
+ return PreferenceHelper.getLocalSubscriptions().contains(channelId)
+ }
+ }
+
+ suspend fun importSubscriptions(newChannels: List) {
+ if (PreferenceHelper.getToken() != "") {
+ try {
+ val token = PreferenceHelper.getToken()
+ RetrofitInstance.authApi.importSubscriptions(
+ false,
+ token,
+ newChannels
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else {
+ val channels = PreferenceHelper.getLocalSubscriptions().toMutableList()
+ newChannels.forEach {
+ if (!channels.contains(it)) channels += it
+ }
+ PreferenceHelper.setLocalSubscriptions(channels)
+ }
+ }
+
+ fun getFormattedLocalSubscriptions(): String {
+ return PreferenceHelper.getLocalSubscriptions().joinToString(",")
+ }
+}
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 f246704c4..952797a37 100644
--- a/app/src/main/java/com/github/libretube/util/ThemeHelper.kt
+++ b/app/src/main/java/com/github/libretube/util/ThemeHelper.kt
@@ -12,13 +12,14 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.text.HtmlCompat
import com.github.libretube.R
import com.github.libretube.preferences.PreferenceHelper
+import com.github.libretube.preferences.PreferenceKeys
import com.google.android.material.color.DynamicColors
object ThemeHelper {
fun updateTheme(activity: AppCompatActivity) {
- val themeMode = PreferenceHelper.getString(activity, "theme_toggle", "A")!!
- val pureThemeEnabled = PreferenceHelper.getBoolean(activity, "pure_theme", false)
+ val themeMode = PreferenceHelper.getString(PreferenceKeys.THEME_MODE, "A")
+ val pureThemeEnabled = PreferenceHelper.getBoolean(PreferenceKeys.PURE_THEME, false)
updateAccentColor(activity, pureThemeEnabled)
updateThemeMode(themeMode)
@@ -30,15 +31,14 @@ object ThemeHelper {
) {
val theme = when (
PreferenceHelper.getString(
- activity,
- "accent_color",
+ PreferenceKeys.ACCENT_COLOR,
"purple"
)
) {
"my" -> {
applyDynamicColors(activity)
- if (pureThemeEnabled) R.style.MaterialYou_Pure
- else R.style.MaterialYou
+ if (pureThemeEnabled) R.style.BaseTheme_Pure
+ else R.style.BaseTheme
}
// set the theme, use the pure theme if enabled
"red" -> if (pureThemeEnabled) R.style.Theme_Red_Pure else R.style.Theme_Red
@@ -72,15 +72,25 @@ object ThemeHelper {
val activityAliases = context.resources.getStringArray(R.array.iconsValue)
// Disable Old Icon(s)
for (activityAlias in activityAliases) {
+ val activityClass = "com.github.libretube." +
+ if (activityAlias == activityAliases[0]) "activities.MainActivity" // default icon/activity
+ else activityAlias
+
+ // remove old icons
context.packageManager.setComponentEnabledSetting(
- ComponentName(context.packageName, "com.github.libretube.$activityAlias"),
+ ComponentName(context.packageName, activityClass),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
+
+ // set the class name for the activity alias
+ val newLogoActivityClass = "com.github.libretube." +
+ if (newLogoActivityAlias == activityAliases[0]) "activities.MainActivity" // default icon/activity
+ else newLogoActivityAlias
// Enable New Icon
context.packageManager.setComponentEnabledSetting(
- ComponentName(context.packageName, "com.github.libretube.$newLogoActivityAlias"),
+ ComponentName(context.packageName, newLogoActivityClass),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
@@ -90,13 +100,15 @@ object ThemeHelper {
fun restartMainActivity(context: Context) {
// kill player notification
val nManager = context
- .getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
+ .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nManager.cancelAll()
- // restart to MainActivity
+ // start a new Intent of the app
val pm: PackageManager = context.packageManager
val intent = pm.getLaunchIntentForPackage(context.packageName)
intent?.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
+ // kill the old application
+ android.os.Process.killProcess(android.os.Process.myPid())
}
fun getThemeColor(context: Context, colorCode: Int): Int {
diff --git a/app/src/main/java/com/github/libretube/util/UpdateChecker.kt b/app/src/main/java/com/github/libretube/util/UpdateChecker.kt
deleted file mode 100644
index 3513a636d..000000000
--- a/app/src/main/java/com/github/libretube/util/UpdateChecker.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.github.libretube.util
-
-import android.util.Log
-import androidx.fragment.app.FragmentManager
-import com.github.libretube.BuildConfig
-import com.github.libretube.GITHUB_API_URL
-import com.github.libretube.dialogs.NoUpdateAvailableDialog
-import com.github.libretube.dialogs.UpdateAvailableDialog
-import com.github.libretube.obj.UpdateInfo
-import org.json.JSONArray
-import org.json.JSONObject
-import java.io.BufferedReader
-import java.io.InputStreamReader
-import java.net.URL
-import javax.net.ssl.HttpsURLConnection
-
-fun checkUpdate(childFragmentManager: FragmentManager) {
- var updateInfo: UpdateInfo? = UpdateInfo("", "")
- // run http request as thread to make it async
- val thread = Thread {
- // otherwise crashes without internet
- try {
- updateInfo = getUpdateInfo()
- } catch (e: Exception) {
- }
- }
- thread.start()
- // wait for the thread to finish
- thread.join()
- // show the UpdateAvailableDialog if there's an update available
- if (updateInfo?.tagName != "" && BuildConfig.VERSION_NAME != updateInfo?.tagName) {
- val updateAvailableDialog = UpdateAvailableDialog(
- updateInfo?.tagName!!,
- updateInfo?.updateUrl!!
- )
- updateAvailableDialog.show(childFragmentManager, "UpdateAvailableDialog")
- } else {
- // otherwise show the no update available dialog
- val noUpdateAvailableDialog = NoUpdateAvailableDialog()
- noUpdateAvailableDialog.show(childFragmentManager, "NoUpdateAvailableDialog")
- }
-}
-
-fun getUpdateInfo(): UpdateInfo? {
- val latest = URL(GITHUB_API_URL)
- val json = StringBuilder()
- val urlConnection: HttpsURLConnection?
- urlConnection = latest.openConnection() as HttpsURLConnection
- val br = BufferedReader(InputStreamReader(urlConnection.inputStream))
-
- var line: String?
- while (br.readLine().also { line = it } != null) json.append(line)
-
- // Parse and return the json data
- val jsonRoot = JSONObject(json.toString())
- if (jsonRoot.has("tag_name") &&
- jsonRoot.has("html_url") &&
- jsonRoot.has("assets")
- ) {
- val updateUrl = jsonRoot.getString("html_url")
- val jsonAssets: JSONArray = jsonRoot.getJSONArray("assets")
- for (i in 0 until jsonAssets.length()) {
- val jsonAsset = jsonAssets.getJSONObject(i)
- if (jsonAsset.has("name")) {
- val name = jsonAsset.getString("name")
- if (name.endsWith(".apk")) {
- val tagName = jsonRoot.getString("name")
- Log.i("", "Latest version: $tagName")
- return UpdateInfo(updateUrl, tagName)
- }
- }
- }
- }
- return null
-}
diff --git a/app/src/main/java/com/github/libretube/views/BottomSheetFragment.kt b/app/src/main/java/com/github/libretube/views/BottomSheetFragment.kt
new file mode 100644
index 000000000..48de8f0ef
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/views/BottomSheetFragment.kt
@@ -0,0 +1,61 @@
+package com.github.libretube.views
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.github.libretube.databinding.BottomSheetBinding
+import com.github.libretube.interfaces.PlayerOptionsInterface
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+
+class BottomSheetFragment : BottomSheetDialogFragment() {
+ private lateinit var binding: BottomSheetBinding
+ private lateinit var playerOptionsInterface: PlayerOptionsInterface
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = BottomSheetBinding.inflate(layoutInflater, container, false)
+ return binding.root
+ }
+
+ fun setOnClickListeners(playerOptionsInterface: PlayerOptionsInterface) {
+ this.playerOptionsInterface = playerOptionsInterface
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.aspectRatio.setOnClickListener {
+ playerOptionsInterface.onAspectRatioClicked()
+ this.dismiss()
+ }
+
+ binding.quality.setOnClickListener {
+ playerOptionsInterface.onQualityClicked()
+ this.dismiss()
+ }
+
+ binding.playbackSpeed.setOnClickListener {
+ playerOptionsInterface.onPlaybackSpeedClicked()
+ this.dismiss()
+ }
+
+ binding.captions.setOnClickListener {
+ playerOptionsInterface.onCaptionClicked()
+ this.dismiss()
+ }
+
+ binding.autoplay.setOnClickListener {
+ playerOptionsInterface.onAutoplayClicked()
+ this.dismiss()
+ }
+
+ binding.repeatMode.setOnClickListener {
+ playerOptionsInterface.onRepeatModeClicked()
+ this.dismiss()
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/views/CustomExoPlayerView.kt b/app/src/main/java/com/github/libretube/views/CustomExoPlayerView.kt
index a3886f8ce..ccad7cdbd 100644
--- a/app/src/main/java/com/github/libretube/views/CustomExoPlayerView.kt
+++ b/app/src/main/java/com/github/libretube/views/CustomExoPlayerView.kt
@@ -5,25 +5,53 @@ import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
+import com.github.libretube.interfaces.DoubleTapInterface
+import com.github.libretube.util.DoubleTapListener
import com.google.android.exoplayer2.ui.StyledPlayerView
+@SuppressLint("ClickableViewAccessibility")
internal class CustomExoPlayerView(
context: Context,
attributeSet: AttributeSet? = null
) : StyledPlayerView(context, attributeSet) {
+ val TAG = "CustomExoPlayerView"
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(event: MotionEvent): Boolean {
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- if (isControllerFullyVisible) {
- hideController()
- } else {
- showController()
- }
- }
+ private var doubleTapListener: DoubleTapInterface? = null
+
+ // the x-position of where the user clicked
+ private var xPos = 0F
+
+ fun setOnDoubleTapListener(
+ eventListener: DoubleTapInterface?
+ ) {
+ doubleTapListener = eventListener
+ }
+
+ private fun toggleController() {
+ if (isControllerFullyVisible) hideController() else showController()
+ }
+
+ val doubleTouchListener = object : DoubleTapListener() {
+ override fun onDoubleClick() {
+ doubleTapListener?.onEvent(xPos)
}
+
+ override fun onSingleClick() {
+ toggleController()
+ }
+ }
+
+ init {
+ // set the double click listener for rewind/forward
+ setOnClickListener(doubleTouchListener)
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ // save the x position of the touch event
+ xPos = event.x
+ // listen for a double touch
+ doubleTouchListener.onClick(this)
return false
}
}
diff --git a/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt b/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt
deleted file mode 100644
index 808ef2320..000000000
--- a/app/src/main/java/com/github/libretube/views/DoubleClickListener.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.github.libretube.views
-
-import android.os.Handler
-import android.os.Looper
-import android.view.View
-
-class DoubleClickListener(
- private val doubleClickTimeLimitMills: Long = 200,
- private val callback: Callback
-) : View.OnClickListener {
- private var lastClicked: Long = -1L
-
- override fun onClick(v: View?) {
- lastClicked = when {
- lastClicked == -1L -> {
- checkForSingleClick()
- System.currentTimeMillis()
- }
- isDoubleClicked() -> {
- callback.doubleClicked()
- -1L
- }
- else -> {
- 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
- }
-
- private fun isDoubleClicked(): Boolean {
- return getTimeDiff(
- lastClicked,
- System.currentTimeMillis()
- ) <= doubleClickTimeLimitMills
- }
-
- interface Callback {
- fun doubleClicked()
- fun singleClicked()
- }
-}
diff --git a/app/src/main/java/com/github/libretube/views/DoubleTapOverlay.kt b/app/src/main/java/com/github/libretube/views/DoubleTapOverlay.kt
new file mode 100644
index 000000000..bdd2a3fcb
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/views/DoubleTapOverlay.kt
@@ -0,0 +1,19 @@
+package com.github.libretube.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import com.github.libretube.databinding.DoubleTapOverlayBinding
+
+class DoubleTapOverlay(
+ context: Context,
+ attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+ var binding: DoubleTapOverlayBinding
+
+ init {
+ val layoutInflater = LayoutInflater.from(context)
+ binding = DoubleTapOverlayBinding.inflate(layoutInflater, this, true)
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/views/MaterialPreferenceFragment.kt b/app/src/main/java/com/github/libretube/views/MaterialPreferenceFragment.kt
new file mode 100644
index 000000000..1bce4578a
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/views/MaterialPreferenceFragment.kt
@@ -0,0 +1,47 @@
+package com.github.libretube.views
+
+import android.os.Bundle
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import com.github.libretube.R
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+/**
+ * PreferenceFragmentCompat using the [MaterialAlertDialogBuilder] instead of the old dialog builder
+ */
+open class MaterialPreferenceFragment : PreferenceFragmentCompat() {
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
+
+ override fun onDisplayPreferenceDialog(preference: Preference) {
+ when (preference) {
+ /**
+ * Show a [MaterialAlertDialogBuilder] when the preference is a [ListPreference]
+ */
+ is ListPreference -> {
+ // get the index of the previous selected item
+ val prefIndex = preference.entryValues.indexOf(preference.value)
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(preference.title)
+ .setSingleChoiceItems(preference.entries, prefIndex) { dialog, index ->
+
+ // get the new ListPreference value
+ val newValue = preference.entryValues[index].toString()
+
+ // save the new value and call the onPreferenceChange Method
+ preference.value = newValue
+ preference.callChangeListener(newValue)
+
+ // dismiss the dialog
+ dialog.dismiss()
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ }
+ /**
+ * Otherwise show the normal dialog, dialogs for other preference types are not supported yet
+ */
+ else -> super.onDisplayPreferenceDialog(preference)
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/views/SingleViewTouchableMotionLayout.kt b/app/src/main/java/com/github/libretube/views/SingleViewTouchableMotionLayout.kt
index 9eb3060bb..a9863d535 100644
--- a/app/src/main/java/com/github/libretube/views/SingleViewTouchableMotionLayout.kt
+++ b/app/src/main/java/com/github/libretube/views/SingleViewTouchableMotionLayout.kt
@@ -26,7 +26,7 @@ class SingleViewTouchableMotionLayout(context: Context, attributeSet: AttributeS
private val transitionListenerList = mutableListOf()
init {
- addTransitionListener(object : MotionLayout.TransitionListener {
+ addTransitionListener(object : TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?,
startId: Int,
@@ -50,7 +50,7 @@ class SingleViewTouchableMotionLayout(context: Context, attributeSet: AttributeS
}
})
- super.setTransitionListener(object : MotionLayout.TransitionListener {
+ super.setTransitionListener(object : TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?,
startId: Int,
diff --git a/app/src/main/res/drawable/ic_add_instance.xml b/app/src/main/res/drawable/ic_add_instance.xml
index d5d34b102..334f007fa 100644
--- a/app/src/main/res/drawable/ic_add_instance.xml
+++ b/app/src/main/res/drawable/ic_add_instance.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_up_left.xml b/app/src/main/res/drawable/ic_arrow_up_left.xml
index e5ed20662..20b455730 100644
--- a/app/src/main/res/drawable/ic_arrow_up_left.xml
+++ b/app/src/main/res/drawable/ic_arrow_up_left.xml
@@ -2,7 +2,7 @@
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ android:fillColor="#FF000000"
+ android:pathData="m28.58,33.834h9.207v-9.352h-2.892v6.46L28.58,30.942ZM10.261,23.518h2.892v-6.46h6.315L19.469,14.166L10.261,14.166ZM7.706,39.715q-1.302,0 -2.29,-0.988Q4.428,37.739 4.428,36.437L4.428,11.563q0,-1.35 0.988,-2.314Q6.405,8.285 7.706,8.285L40.294,8.285q1.35,0 2.314,0.964 0.964,0.964 0.964,2.314v24.874q0,1.302 -0.964,2.29 -0.964,0.988 -2.314,0.988zM7.706,36.437L40.294,36.437L40.294,11.563L7.706,11.563ZM7.706,36.437L7.706,11.563Z" />
diff --git a/app/src/main/res/drawable/ic_auth.xml b/app/src/main/res/drawable/ic_auth.xml
index 0d39a3ad7..4386e435d 100644
--- a/app/src/main/res/drawable/ic_auth.xml
+++ b/app/src/main/res/drawable/ic_auth.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_caption_outlined.xml b/app/src/main/res/drawable/ic_caption_outlined.xml
new file mode 100644
index 000000000..d264aed1f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_caption_outlined.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_color.xml b/app/src/main/res/drawable/ic_color.xml
index ae57a36aa..2dc279642 100644
--- a/app/src/main/res/drawable/ic_color.xml
+++ b/app/src/main/res/drawable/ic_color.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
index 05fc2abd8..d78e8d21b 100644
--- a/app/src/main/res/drawable/ic_delete.xml
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -2,10 +2,9 @@
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
- android:viewportWidth="24"
- android:viewportHeight="24">
-
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ android:pathData="M13.05,42q-1.2,0 -2.1,-0.9 -0.9,-0.9 -0.9,-2.1L10.05,10.5L8,10.5v-3h9.4L17.4,6h13.2v1.5L40,7.5v3h-2.05L37.95,39q0,1.2 -0.9,2.1 -0.9,0.9 -2.1,0.9ZM18.35,34.7h3L21.35,14.75h-3ZM26.65,34.7h3L29.65,14.75h-3Z" />
diff --git a/app/src/main/res/drawable/ic_discord.xml b/app/src/main/res/drawable/ic_discord.xml
index 4da87697f..95f0a7a33 100644
--- a/app/src/main/res/drawable/ic_discord.xml
+++ b/app/src/main/res/drawable/ic_discord.xml
@@ -1,7 +1,7 @@
+ android:fillColor="#FF000000"
+ android:pathData="M12.322,1.707 L16.615,6 12.322,10.293L12.322,7A0.292,0.292 0,0 0,12.029 6.707c-3.468,0 -6.293,2.825 -6.293,6.293 -0,3.468 2.825,6.293 6.293,6.293 3.368,0 6.107,-2.67 6.264,-6L19.707,13.293C19.55,17.416 16.191,20.707 12.029,20.707 7.767,20.707 4.322,17.262 4.322,13 4.322,8.738 7.767,5.293 12.029,5.293A0.292,0.292 0,0 0,12.322 5Z" />
diff --git a/app/src/main/res/drawable/ic_frame.xml b/app/src/main/res/drawable/ic_frame.xml
index 7f0e915ba..92447508d 100644
--- a/app/src/main/res/drawable/ic_frame.xml
+++ b/app/src/main/res/drawable/ic_frame.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/app/src/main/res/drawable/ic_label.xml b/app/src/main/res/drawable/ic_label.xml
index 5f0035189..ca87759c5 100644
--- a/app/src/main/res/drawable/ic_label.xml
+++ b/app/src/main/res/drawable/ic_label.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml
new file mode 100644
index 000000000..79972afe5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_next.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 000000000..7ffabd030
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_open.xml b/app/src/main/res/drawable/ic_open.xml
new file mode 100644
index 000000000..a5f1c87be
--- /dev/null
+++ b/app/src/main/res/drawable/ic_open.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml
index 645e31d2c..b92538943 100644
--- a/app/src/main/res/drawable/ic_pause.xml
+++ b/app/src/main/res/drawable/ic_pause.xml
@@ -4,7 +4,7 @@
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/ic_pause_filled.xml b/app/src/main/res/drawable/ic_pause_filled.xml
index 39b09cf20..1686afa0a 100644
--- a/app/src/main/res/drawable/ic_pause_filled.xml
+++ b/app/src/main/res/drawable/ic_pause_filled.xml
@@ -1,7 +1,7 @@
diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml
index 6e5745be5..da896608b 100644
--- a/app/src/main/res/drawable/ic_play.xml
+++ b/app/src/main/res/drawable/ic_play.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_reddit.xml b/app/src/main/res/drawable/ic_reddit.xml
index ab73945f7..2e3cd4889 100644
--- a/app/src/main/res/drawable/ic_reddit.xml
+++ b/app/src/main/res/drawable/ic_reddit.xml
@@ -1,7 +1,7 @@
+ android:fillColor="#FF000000"
+ android:pathData="M11.707,1.707 L7.414,6 11.707,10.293V7A0.292,0.292 0,0 1,12 6.707c3.468,0 6.293,2.825 6.293,6.293 0,3.468 -2.825,6.293 -6.293,6.293 -3.368,0 -6.107,-2.67 -6.264,-6H4.322C4.479,17.416 7.838,20.707 12,20.707 16.262,20.707 19.707,17.262 19.707,13 19.707,8.738 16.262,5.293 12,5.293A0.292,0.292 0,0 1,11.707 5Z" />
diff --git a/app/src/main/res/drawable/ic_rotating_circle.xml b/app/src/main/res/drawable/ic_rotating_circle.xml
index 63ad1c16a..d92f992c8 100644
--- a/app/src/main/res/drawable/ic_rotating_circle.xml
+++ b/app/src/main/res/drawable/ic_rotating_circle.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_server.xml b/app/src/main/res/drawable/ic_server.xml
index e7cb848fa..9f33cc94f 100644
--- a/app/src/main/res/drawable/ic_server.xml
+++ b/app/src/main/res/drawable/ic_server.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_skip.xml b/app/src/main/res/drawable/ic_skip.xml
index 1594f7d64..33b89f5da 100644
--- a/app/src/main/res/drawable/ic_skip.xml
+++ b/app/src/main/res/drawable/ic_skip.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_toggle_on.xml b/app/src/main/res/drawable/ic_toggle_on.xml
new file mode 100644
index 000000000..d19f35406
--- /dev/null
+++ b/app/src/main/res/drawable/ic_toggle_on.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_trending.xml b/app/src/main/res/drawable/ic_trending.xml
index 536738d66..233823167 100644
--- a/app/src/main/res/drawable/ic_trending.xml
+++ b/app/src/main/res/drawable/ic_trending.xml
@@ -1,7 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/ic_weblate.xml b/app/src/main/res/drawable/ic_weblate.xml
new file mode 100644
index 000000000..beaa5cb5a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_weblate.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bottom_sheet.xml b/app/src/main/res/drawable/rounded_bottom_sheet.xml
new file mode 100644
index 000000000..a911e59af
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_bottom_sheet.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/activity_about.xml
similarity index 87%
rename from app/src/main/res/layout/fragment_about.xml
rename to app/src/main/res/layout/activity_about.xml
index d09cf88f7..e9c7e5914 100644
--- a/app/src/main/res/layout/fragment_about.xml
+++ b/app/src/main/res/layout/activity_about.xml
@@ -81,6 +81,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_community.xml b/app/src/main/res/layout/activity_community.xml
similarity index 98%
rename from app/src/main/res/layout/fragment_community.xml
rename to app/src/main/res/layout/activity_community.xml
index 6651fbd90..8b7715693 100644
--- a/app/src/main/res/layout/fragment_community.xml
+++ b/app/src/main/res/layout/activity_community.xml
@@ -6,7 +6,7 @@
-
- />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/channel_search_row.xml b/app/src/main/res/layout/channel_row.xml
similarity index 100%
rename from app/src/main/res/layout/channel_search_row.xml
rename to app/src/main/res/layout/channel_row.xml
diff --git a/app/src/main/res/layout/channel_subscription_row.xml b/app/src/main/res/layout/channel_subscription_row.xml
index 591facca0..7959fbefa 100644
--- a/app/src/main/res/layout/channel_subscription_row.xml
+++ b/app/src/main/res/layout/channel_subscription_row.xml
@@ -1,6 +1,7 @@
+ android:textSize="16sp"
+ tools:text="Channel Name" />
+ app:cornerRadius="20dp"
+ app:elevation="20dp" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/chapter_column.xml b/app/src/main/res/layout/chapter_column.xml
index 595130de6..243941ae7 100644
--- a/app/src/main/res/layout/chapter_column.xml
+++ b/app/src/main/res/layout/chapter_column.xml
@@ -1,27 +1,37 @@
-
+ android:backgroundTint="@android:color/transparent"
+ app:strokeWidth="0dp">
-
+
-
+
-
\ No newline at end of file
+
+
+
+
+
\ 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 5256e49d6..2a4602244 100644
--- a/app/src/main/res/layout/comments_row.xml
+++ b/app/src/main/res/layout/comments_row.xml
@@ -1,6 +1,7 @@
@@ -45,9 +46,9 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
- android:text="Author and Time"
android:textSize="15sp"
- android:textStyle="bold" />
+ android:textStyle="bold"
+ tools:text="Author and Time" />
+ tools:text="Comment Text" />
+ tools:text="LikeCount" />
-
+
+
+
+
+
+
+
+
+ android:layout_margin="8dp"
+ android:visibility="gone" />
diff --git a/app/src/main/res/layout/double_tap_overlay.xml b/app/src/main/res/layout/double_tap_overlay.xml
new file mode 100644
index 000000000..717b05544
--- /dev/null
+++ b/app/src/main/res/layout/double_tap_overlay.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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 8df359629..9741d8d52 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
@@ -1,6 +1,7 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ android:paddingStart="5dp"
+ android:paddingTop="5dp"
+ android:paddingEnd="10dp">
-
+
-
+
-
+
-
-
-
-
-
+
+ android:id="@+id/exo_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginHorizontal="10dp"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@android:color/white"
+ android:textSize="18sp"
+ android:visibility="invisible" />
-
+
+
+
+
+
@@ -87,33 +85,66 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
- android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top"
+ android:layout_marginTop="10dp"
android:orientation="vertical">
+ android:baselineAligned="false"
+ android:paddingStart="10dp"
+ android:paddingEnd="20dp">
+ android:layout_marginStart="10dp">
+ style="@style/TimeString"
+ tools:text="05:20" />
-
+
+ style="@style/TimeString"
+ tools:text="12:15" />
+
+
+
+
+
+
+
+
+
+
@@ -150,16 +181,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
-
-
-
-
+ android:padding="20dp">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml
index 45b4a8ff7..14a75275a 100644
--- a/app/src/main/res/layout/fragment_channel.xml
+++ b/app/src/main/res/layout/fragment_channel.xml
@@ -39,8 +39,8 @@
@@ -57,9 +57,9 @@
android:drawablePadding="3dip"
android:ellipsize="end"
android:maxLines="1"
- android:text="Channel Name"
android:textSize="16sp"
- android:textStyle="bold" />
+ android:textStyle="bold"
+ tools:text="Channel Name" />
@@ -78,12 +78,15 @@
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:drawableLeft="@drawable/ic_bell_small"
+ android:drawableStart="@drawable/ic_bell_small"
android:drawableTint="?android:attr/textColorPrimary"
+ android:stateListAnimator="@null"
android:text="@string/subscribe"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
- app:cornerRadius="20dp" />
+ app:cornerRadius="20dp"
+ app:elevation="20dp"
+ tools:targetApi="m" />
@@ -94,8 +97,7 @@
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="15dp"
- android:autoLink="web"
- android:text="" />
+ android:autoLink="web" />
diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml
index 6584543e2..a58fff9e5 100644
--- a/app/src/main/res/layout/fragment_player.xml
+++ b/app/src/main/res/layout/fragment_player.xml
@@ -41,8 +41,8 @@
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="30dp"
- android:text="Video Title"
- android:textSize="18sp" />
+ android:textSize="18sp"
+ tools:text="Video Title" />
+ tools:text="10M views 2 days ago " />
+ tools:text="4.2K" />
+ tools:text="1.3K" />
@@ -185,14 +185,14 @@
+ android:src="@drawable/ic_open" />
+ android:textSize="15sp" />
-
+ android:textSize="17sp" />
+ android:src="@drawable/ic_arrow_up_down" />
-
+
@@ -377,61 +373,16 @@
app:layout_constraintTop_toTopOf="@id/main_container"
app:show_buffering="when_playing">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center" />
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_playlist.xml b/app/src/main/res/layout/fragment_playlist.xml
index 5b30b3347..821883788 100644
--- a/app/src/main/res/layout/fragment_playlist.xml
+++ b/app/src/main/res/layout/fragment_playlist.xml
@@ -20,7 +20,7 @@
@@ -43,20 +42,20 @@
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
- android:layout_marginRight="10dp"
+ android:layout_marginEnd="10dp"
android:src="@drawable/ic_three_dots" />
diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml
index 1cd0329b1..ed0f56f3a 100644
--- a/app/src/main/res/layout/fragment_search.xml
+++ b/app/src/main/res/layout/fragment_search.xml
@@ -1,349 +1,39 @@
-
-
+ android:layout_marginVertical="10dp" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:layout_gravity="center"
+ android:layout_marginVertical="10dp"
+ android:src="@drawable/ic_history" />
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search_result.xml b/app/src/main/res/layout/fragment_search_result.xml
new file mode 100644
index 000000000..2b1cd3735
--- /dev/null
+++ b/app/src/main/res/layout/fragment_search_result.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml
index 68a377c56..6e27f08f0 100644
--- a/app/src/main/res/layout/fragment_subscriptions.xml
+++ b/app/src/main/res/layout/fragment_subscriptions.xml
@@ -1,5 +1,6 @@
+ android:layout_centerInParent="true"
+ android:visibility="gone">
+ android:src="@drawable/ic_list" />
@@ -53,53 +55,93 @@
-
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="5dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="12dp"
+ android:visibility="gone"
+ app:cardCornerRadius="18dp"
+ app:cardElevation="20dp">
-
+ android:layout_margin="8dp">
-
+
-
+
+
+
+
+
+ android:descendantFocusability="blocksDescendants"
+ android:visibility="gone">
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_watch_history.xml b/app/src/main/res/layout/fragment_watch_history.xml
index 3e522b29d..da62ec3cc 100644
--- a/app/src/main/res/layout/fragment_watch_history.xml
+++ b/app/src/main/res/layout/fragment_watch_history.xml
@@ -1,43 +1,37 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
+ android:layout_marginHorizontal="10dp"
+ android:gravity="center"
+ android:text="@string/history_empty"
+ android:textSize="20sp"
+ android:textStyle="bold" />
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/playlist_row.xml b/app/src/main/res/layout/playlist_row.xml
index cca5c2238..9a69f8594 100644
--- a/app/src/main/res/layout/playlist_row.xml
+++ b/app/src/main/res/layout/playlist_row.xml
@@ -36,27 +36,40 @@
android:layout_height="match_parent"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
-
+ android:layout_gravity="bottom"
+ android:orientation="vertical">
-
+ android:layout_gravity="end"
+ android:layout_marginEnd="5dp"
+ android:layout_marginBottom="5dp"
+ app:cardBackgroundColor="@color/duration_background_color"
+ app:cardCornerRadius="8dp"
+ app:cardElevation="0dp">
+
+
+
+
+
+
+
+
-
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Playlist Name" />
+ app:layout_constraintTop_toBottomOf="@+id/playlist_title"
+ tools:text="Description" />
@@ -52,8 +51,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
- android:text=""
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/delete_playlist"
app:layout_constraintStart_toEndOf="@+id/card_playlist_thumbnail"
app:layout_constraintTop_toBottomOf="@+id/playlist_title" />
diff --git a/app/src/main/res/layout/replies_row.xml b/app/src/main/res/layout/replies_row.xml
index 6c46ed634..f8d76b0af 100644
--- a/app/src/main/res/layout/replies_row.xml
+++ b/app/src/main/res/layout/replies_row.xml
@@ -1,6 +1,7 @@
@@ -34,9 +35,9 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
- android:text="Author and Time"
android:textSize="15sp"
- android:textStyle="bold" />
+ android:textStyle="bold"
+ tools:text="Author and Time" />
+ tools:text="Comment Text" />
+ tools:text="LikeCount" />
+ android:layout_height="match_parent" />
-
+ android:layout_gravity="bottom"
+ android:orientation="vertical">
-
+ android:layout_gravity="end"
+ android:layout_marginEnd="5dp"
+ android:layout_marginBottom="5dp"
+ app:cardBackgroundColor="@color/duration_background_color"
+ app:cardCornerRadius="8dp"
+ app:cardElevation="0dp">
-
+
+
+
+
+
+
+
@@ -60,22 +71,22 @@
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="2"
- android:text="Title"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/thumbnailcard"
app:layout_constraintStart_toEndOf="@+id/channel_image"
- app:layout_constraintTop_toBottomOf="@+id/thumbnailcard" />
+ app:layout_constraintTop_toBottomOf="@+id/thumbnailcard"
+ tools:text="Title" />
+ app:layout_constraintTop_toBottomOf="@+id/textView_title"
+ tools:text="Channel Name" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/video_search_row.xml b/app/src/main/res/layout/video_row.xml
similarity index 60%
rename from app/src/main/res/layout/video_search_row.xml
rename to app/src/main/res/layout/video_row.xml
index 8d504f433..c94c725c5 100644
--- a/app/src/main/res/layout/video_search_row.xml
+++ b/app/src/main/res/layout/video_row.xml
@@ -16,7 +16,7 @@
app:layout_constraintGuide_percent=".45" />
-
+ android:layout_gravity="bottom"
+ android:orientation="vertical">
-
+ android:layout_gravity="end"
+ android:layout_marginEnd="5dp"
+ android:layout_marginBottom="5dp"
+ app:cardBackgroundColor="@color/duration_background_color"
+ app:cardCornerRadius="8dp"
+ app:cardElevation="0dp">
-
+
+
+
+
+
+
+
+ app:layout_constraintStart_toEndOf="@id/thumbnail_card"
+ app:layout_constraintTop_toBottomOf="@id/video_title" />
+ app:layout_constraintTop_toBottomOf="@id/video_info" />
-
+ app:layout_constraintStart_toEndOf="@id/channel_image"
+ app:layout_constraintTop_toBottomOf="@id/video_info" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/watch_history_row.xml b/app/src/main/res/layout/watch_history_row.xml
index 1de9b89eb..935836a85 100644
--- a/app/src/main/res/layout/watch_history_row.xml
+++ b/app/src/main/res/layout/watch_history_row.xml
@@ -35,29 +35,39 @@
android:layout_height="match_parent"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
-
+ android:layout_gravity="bottom"
+ android:orientation="vertical">
-
+ android:layout_gravity="end"
+ android:layout_marginEnd="5dp"
+ android:layout_marginBottom="5dp"
+ app:cardBackgroundColor="@color/duration_background_color"
+ app:cardCornerRadius="8dp"
+ app:cardElevation="0dp">
-
+
+
+
+
+
+
+
@@ -68,16 +78,16 @@
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="2"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/deleteBTN"
app:layout_constraintStart_toEndOf="@id/thumbnail_card"
app:layout_constraintTop_toTopOf="parent" />
@@ -88,7 +98,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@+id/guideline"
- app:layout_constraintTop_toBottomOf="@id/upload_date" />
+ app:layout_constraintTop_toBottomOf="@id/video_info" />
+ app:layout_constraintTop_toBottomOf="@id/video_info" />
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/action_bar.xml b/app/src/main/res/menu/action_bar.xml
index a5d77e155..9d47ac612 100644
--- a/app/src/main/res/menu/action_bar.xml
+++ b/app/src/main/res/menu/action_bar.xml
@@ -4,8 +4,25 @@
+ app:actionViewClass="androidx.appcompat.widget.SearchView"
+ app:showAsAction="ifRoom|collapseActionView" />
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav.xml b/app/src/main/res/navigation/nav.xml
index 50e5f51cb..90a00d2be 100644
--- a/app/src/main/res/navigation/nav.xml
+++ b/app/src/main/res/navigation/nav.xml
@@ -19,12 +19,17 @@
android:id="@+id/libraryFragment"
android:name="com.github.libretube.fragments.LibraryFragment"
android:label="fragment_library"
- tools:layout="@layout/fragment_library">
+ tools:layout="@layout/fragment_library" />
+
- اختر نموذج
- نموذج مخصص
+ اختر…
+ مخصص
الجودة
تم إنشاء حساب بنجاح! تستطيع الآن أن تشترك بالقنوات التي تريدها.
بحث
@@ -34,7 +34,7 @@
سمة التطبيق
لا يمكن لاسم المستخدم وكلمة المرور أن يكونوا فارغين.
هذا لحساب Piped.
- جودة الفيديو الافتراضية
+ دقة الفيديو
أعمدة الشبكة
لا شيء هنا.
حذف قائمة التشغيل
@@ -141,7 +141,7 @@
الإصدار %1$s
تعرف على فريق LibreTube وكيف يحدث كل شيء.
المحتويات ذات الصلة
- اعرض التدفقات ذات الصلة بجانب ما تشاهده.
+ اعرض تدفقات مماثلة إلى جانب ما تشاهده.
عرض الفصول
إخفاء الفصول
حشو لاداع له / او النكات
@@ -162,9 +162,9 @@
تبرّع
الإرث الضائع
التدرج اللوني
- الأنابيب ، تسجيل الدخول ، الاشتراكات
+ الأنابيب وتسجيل الدخول والاشتراكات
اسم المثيل
- مسح المثيلات المخصصة
+ مسح المضاف
مباشر
التشغيل التلقائي
انتقل إلى الإصدارات على GitHub لتنزيله؟
@@ -178,30 +178,30 @@
المشغل
GPLv3+ هو ترخيص حر متروك للحقوق. الاستخدام والدراسة والتغيير والمشاركة؛ مع الجميع.
أعط ما يستحقه هذا لك ، إذا استطعت. فريق LibreTube أصغر من تبرعك أو مساعدتك.
- سرعة التشغيل الافتراضية
+ سرعة التشغيل
اضبط التطبيق حسب رغبتك.
تعرف على كل من يشارك في تطوير التطبيق وتحسينه.
- التنزيلات والتاريخ
- إضافة مثيل مخصص (على مسؤوليتك الخاصة)
+ التنزيلات وإعادة التعيين
+ أضف…
مجلد الأفلام
مشاركة عنوان URL إلى
لا يوجد صوت
لا يوجد فيديو
صوت
فيديو
- قيد التحميل
+ تحميل…
إخفاء صفحة المحتوى الشائع
عنوان URL لمثيل الواجهة الأمامية
الجودة
السلوك
الجودة وسلوك المشغل
معدل التقديم
- Piped هي واجهة ويب بديلة مفتوحة المصدر ل YouTube توفر واجهة برمجة التطبيقات التي نستخدمها. بدون Piped ، لن يكون LibreTube موجودا. شكرا جزيلا لمطوريهم!
+ Piped هي واجهة ويب بديلة مجانية ل YouTube توفر واجهة برمجة التطبيقات التي نستخدمها. بدونها ، لن يكون LibreTube موجودا. شكرا جزيلا لمطوريهم!
الإيقاف المؤقت التلقائي
- قم بتشغيل الفيديو التالي تلقائيا عند الانتهاء من الحالي.
- أوقف المشغل مؤقتا عند إيقاف تشغيل الشاشة.
+ التشغيل التلقائي للفيديو التالي بعد الفيديو الحالي.
+ أوقف التشغيل مؤقتا عند إيقاف تشغيل الشاشة.
استعادة الإعدادات الافتراضية
- هل أنت متأكد؟ سيؤدي ذلك إلى تسجيل خروجك وإعادة تعيين جميع إعداداتك!
+ إعادة تعيين جميع الإعدادات وتسجيل الخروج؟
نسخ قائمة التشغيل
حذف الحساب
الحساب
@@ -209,7 +209,7 @@
حذف حساب Piped الخاص بك
سجل المشاهدة
تذكر مكان التوقف
- تذكر مكان التوقف وابحث عنه تلقائيا.
+ المتابعة من موضع التشغيل الأخير
مصادقة المثيل
استخدم مثيلا مختلفا للمكالمات المصادق عليها.
اختر مثيل المصادقة
@@ -217,8 +217,8 @@
جيت هب
الصوت والفيديو
اتجاه ملء الشاشة
- نسبة أبعاد الفيديو
- الدوران التلقائي
+ نسبة العرض إلى الارتفاع لمقاطع الفيديو
+ التدوير التلقائي
افقي
عمودي
المجتمع
@@ -227,6 +227,74 @@
تيليجرام
ريديت
تويتر
- يرجى الاتصال بالإنترنت عن طريق تشغيل WiFi أو بيانات الجوال.
+ يرجى تشغيل Wi-Fi أو بيانات الجوال للاتصال بالإنترنت.
فتح …
+ الفصول
+ سرعة التشغيل
+ إعادة تشغيل التطبيق؟
+ إعادة تشغيل التطبيق مطلوبة
+ رؤية ملصق شريط التنقل
+ دائماً
+ المحدد
+ أبداً
+ ملء الشاشة تلقائيا
+ التشغيل بملء الشاشة عند تدوير الجهاز.
+ نسق نقي
+ نسق أبيض/أسود نقي
+ لم يتم العثور على مشغل خارجي. يرجى التأكد من تثبيته.
+ وضع توفير البيانات
+ تذكر عمليات البحث
+ تتبع مقاطع الفيديو التي تمت مشاهدتها محليا
+ تذكر مواضع التشغيل
+ اعادة تعيين
+ نمط التسمية التوضيحية للنظام
+ التسميات التوضيحيه
+ لاشيء
+ تخطي الصور المصغرة والصور الأخرى.
+ سجل المشاهدة والبحث
+ قم بتثبيت إصدار LibreTube الجديد الآن؟
+ معاينة الفيديو
+ إظهار معاينة عند سحب مؤشر التشغيل.
+ عام
+ اللغة والمنطقة
+ قيد التشغيل في الخلفية…
+ التسميات التوضيحية
+ تحميل APK…
+ تنسيق الصوت للمشغل
+ جودة الصوت
+ الأفضل
+ أسوأ
+ لغة الترجمة
+ الإشعارات
+ إشعارات لأحداث البث الجديدة
+ إشعارات حول المحتوى الجديد من منشئي المحتوى الذين تتابعهم.
+ تحقق من كل…
+ %1$s محتوى بث جديد متاح
+ بث جديد من قبل %1$s …
+ هل أنت متأكد؟ هذا لا يمكن التراجع عنه!
+ لا يوجد سجل حتى الان.
+ الأحدث
+ الأقدم
+ الفرز
+ أقل عدد من المشاهدات
+ اسم القناة (Z-A)
+ معظم المشاهدات
+ اسم القناة (من الألف إلى الياء)
+ اتصال مطلوب
+ الجميع
+ مقننة
+ واي فاي فقط
+ الترجمة
+ المساعدة في ترجمة التطبيق
+ لا توجد نتائج.
+ خطأ
+ تم النسخ
+ المشاركة مع وقت البدء
+ نجح التحميل
+ تصدير الاشتراكات
+ أزرار التخطي
+ إظهار الأزرار للتخطي إلى الفيديو التالي أو السابق.
+ الحد الأقصى لحجم السجل
+ غير محدود
+ وضع الخلفية
\ 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 e2091a974..e648caee1 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -23,8 +23,8 @@
Ev
Abunəliklər
Kitabxana
- Server seçin
- Fərdi server
+ Seçin…
+ Fərdi
Məkan
Hamısı
Axtarış filtrini seçin
@@ -78,7 +78,7 @@
Nəsə xəta baş verdi.
Siz istifadəçi adı və şifrə daxil etməlisiniz.
Bu, Piped hesabı üçündür.
- Defolt video keyfiyyəti
+ Video keyfiyyəti
Şəbəkə sütunları
Burada heç nə yoxdur.
Pleylisti silin
@@ -133,11 +133,11 @@
Mümkünsə, bunun sizə nə dəyəri varsa verin. LibreTube komandası ianə və ya yardımınızdan daha kiçikdir.
Yeni versiyanı axtarın
Ən son versiya işlədilir.
- Defolt oynatma sürəti
+ Oynatma sürəti
Qabaqcıl
Oynadıcı
Tətbiqi öz zövqünüzə uyğunlaşdırın.
- Endirmələr, tarixçə
+ Endirmələr və sıfırlama
Tətbiqin güncəl olub-olmadığını öyrənmək üçün klikləyin.
Ən son versiyanı işlədirsiniz.
Canlı
@@ -165,14 +165,14 @@
Server API URL\'si
Server Əlavə Edin
Zəhmət olmasa, işləyən URL\'ni daxil edin
- Fərdi server əlavə edin (riski özünüzə aiddir)
- Piped, giriş, abunəliklər
+ Əlavə edin…
+ Piped, giriş və abunəliklər
Adı və API URL\'ni daxil edin.
- Fərdi serverləri silin
- Versiya %1$s
+ Əlavələri silin
+ V %1$s
LibreTube komandasını tanıyın və hər şeyin necə baş verdiyini öyrənin.
Əlaqədar məzmun
- İzlədiyinizlə yan-yana əlaqəli yayımları göstərin.
+ Baxdığınızla yanaşı oxşar yayımları göstərin.
Əlaqəsiz/Zarafatlar
Bölmələri göstərin
Musiqi: Musiqisiz Bölmə
@@ -186,7 +186,7 @@
Ön yükləmə
Avto-oynatma
Video yoxdur
- Endirilir
+ Endirilir…
Video
Səs yoxdur
Səs
@@ -195,21 +195,21 @@
Davranış
Keyfiyyət və oynadıcı davranışı
Axtarışı artırın
- Piped, istifadə etdiyimiz API\'ni təmin edən YouTube üçün açıq mənbəli alternativ veb interfeysidir. Piped olmasaydı LibreTube olmazdı. Tərtibatçılarına böyük təşəkkürlər!
+ Piped, istifadə etdiyimiz API\' ni təmin edən YouTube üçün pulsuz alternativ veb interfeysidir. Onsuz LibreTube mövcud olmazdı. Onların tərtibatçılarına böyük təşəkkürlər!
Server üçün URL
- Cari bitdikdə növbəti videonu avtomatik olaraq oynadın.
+ Cari videodan sonra növbəti videonu avtomatik oynadın.
Avto-fasilə
- Ekran söndürüldükdə oynadıcını dayandırın.
+ Ekran söndürüldükdə oynatmanı dayandırın.
Pleylisti klonlayın
İlkin tənzimləmələri bərpa edin
- Siz əminsiniz\? Bu, sizi sistemdən çıxaracaq və bütün tənzimləmələrinizi sıfırlayacaq!
+ Bütün tənzimləmələr sıfırlansın və sistemdən çıxılsın\?
Hesab
Bərpa Edin
Hesabı silin
Piped hesabınızı silin
Baxış Tarixçəsi
Mövqeyi xatırlayın
- İzləmə mövqeyini xatırlayın və avtomatik olaraq onu axtarın.
+ Son oynatma mövqeyindən davam edin
Təsdiqləmə serveri seçin
Təsdiqlənmiş dəvətlər üçün fərqli serverdən istifadə edin.
Təsdiqləmə serveri
@@ -217,8 +217,8 @@
Avtomatik
Səs və video
Tam ekran istiqaməti
- Video aspekt nisbəti
- Avtomatik fırlatma
+ Videolar üçün aspekt nisbəti
+ Avtomatik-fırlatma
Portret
Landşaft
İctimaiyyət
@@ -228,6 +228,67 @@
Twitter
Discord
Açıq…
- Zəhmət olmasa, WiFi və ya mobil datanı yandırmaqla internetə qoşulun.
+ İnternetə qoşulmaq üçün Wi-Fi və ya mobil datanı yandırın.
Bölmələr
+ Oynatma sürəti
+ Tətbiq yenidən başladılsın \?
+ Tətbiqi yenidən başlatmaq tələb olunur
+ Datanı saxlamaq üçün miniatürlər və digər şəkilləri yükləməyin.
+ Baxış və axtarış tarixçəsi
+ Naviqasiya etiketinin görünməsi
+ Həmişə
+ Seçildi
+ Heç vaxt
+ Avtomatik-tam ekran
+ Təmiz mövzu
+ Təmiz ağ/qara mövzu
+ Xarici oynadıcı tapılmadı. Zəhmət olmasa, birinin quraşdırıldığından əmin olun.
+ Dataya qənaət rejimi
+ Cihaz fırladıldıqda avtomatik olaraq tam ekran oynadıcıya keçin.
+ Axtarış sorğularını yerli olaraq saxlayın
+ Yerli olaraq baxılmış videoları izləyin
+ Baxış mövqelərini sıfırlayın
+ Sistem altyazı üsulu
+ Altyazılar
+ Heç biri
+ Baxış mövqeləri
+ Tətbiqi indi yeniləmək istəyirsiniz\?
+ Axtarış çubuğu önizləməsi
+ Axtarış çubuğunu ovuşdurarkən mövqeyi axtararaq videonu önizləmə.
+ Dil, məkan
+ Ümumi
+ Arxa fonda oynadılır…
+ Altyazı tənzimləmələri
+ Apk endirilir …
+ Oynadıcı üçün səs formatı
+ Səs keyfiyyəti
+ Ən yaxşı keyfiyyət
+ Ən pis keyfiyyət
+ Defolt altyazı dili
+ Bildirişlər
+ Yeni yayım bildirişləri
+ Abunəliklərdən yeni yayımlar buraxıldıqda bildirin
+ Yoxlama tezliyi
+ %1$s yeni yayım mövcuddur
+ Siz əminsiniz\? Bu geri qaytarıla bilməz!
+ Ən yeni
+ Ən köhnə
+ Ən çox baxılan
+ Ən az baxılan
+ Kanal Adı (A-Z)
+ Kanal Adı (Z-A)
+ Növ
+ %1$s tərəfindən yeni yayımlar…
+ Tarixçə boşdur.
+ Tələb olunan şəbəkə növü
+ Bütün şəbəkələr
+ Yalnız WiFi
+ Tərcümə etmək
+ Tətbiqi danışdığınız dilə tərcümə edərək kömək edin
+ Ölçülmüş
+ Heç bir nəticə tapılmadı.
+ Kopyalandı
+ Xəta baş verdi
+ Endirmə uğurlu oldu
+ Vaxt kodu ilə paylaşın
\ No newline at end of file
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
new file mode 100644
index 000000000..febe08bc1
--- /dev/null
+++ b/app/src/main/res/values-ca/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Biblioteca
+ Sí
+ Cerca
+ Vídeos
+ Comparteix
+ Desa
+ Nom d\'usuari
+ Contrasenya
+ Subscriure\'s
+ Cancel·la la subscripció
+ Inici
+ Subscripcions
+ Descarrega
+
\ No newline at end of file
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index f73b5a73e..748056c3a 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -5,7 +5,7 @@
ODHLÁSIT ODBĚR
Ano
Uživatelské jméno
- Vyberte kvalitu:
+ Kvalita
Sdílet
Heslo
Registrace proběhla úspěšně! Nyní se můžete přihlásit k odběru kanálů.
@@ -14,9 +14,9 @@
Zrušit
Úspěšně přihlášen.
Jste již přihlášeni, můžete se odhlásit ze svého účtu.
- Přihlaste se prosím a zkuste to znovu!
- Výběr instance
- Přidání vlastní instance
+ Přihlaste se a zkuste to znovu.
+ Vybrat…
+ Vlastní
Úspěšně přihlášeno k odběru
Stahování dokončeno.
Stahování se nezdařilo.
@@ -24,44 +24,44 @@
Server zaznamenal problém. Možná zkuste jinou instanci.
Nelze otevřít ve VLC. Možná není nainstalováno.
Chyba sítě.
- Výběr sloupců mřížky
+ Sloupce mřížky
Nic tu není.
- Smazat seznam skladeb
- Opravdu chcete smazat tento seznam videí\?
- Vytvořit seznam skladeb
- Seznam skladeb vytvořen!
- Název seznamu skladeb
- Přidat do seznamu skladeb
- Úspěch!
+ Odstranit playlist
+ Opravdu chcete odstranit tento playlist\?
+ Vytvořit playlist
+ Playlist vytvořen.
+ Název playlistu
+ Přidat do playlistu
+ Hotovo.
Nepodařilo se :(
Stáhnout
Uložit
Přihlásit se
Úspěšně odhlášen.
- Výběr oblasti
+ Oblast
Nejprve se prosím v nastavení přihlaste nebo zaregistrujte!
Tento stream nelze stáhnout.
- Již probíhá další stahování, počkejte prosím, až bude dokončeno.
+ Již probíhá další stahování, počkejte prosím, než bude dokončeno.
Něco se pokazilo.
- Přihlásit/Registrovat
+ Přihlásit/registrovat
Nejprve se přihlaste k odběru některých kanálů.
- Výchozí rozlišení videa
+ Rozlišení videa
Otevřít ve VLC
Importovat odběry
Uživatelské jméno a heslo nesmí být prázdné.
- Toto není váš účet Gmail.
+ Toto je pro účet Piped.
O aplikaci
- Název seznamu skladeb nesmí být prázdný
+ Název playlistu nesmí být prázdný
Z YouTube nebo NewPipe
Videa
- Tmavý motiv
- Světlý motiv
+ Tmavý
+ Světlý
Trendy
Knihovna
- Odebírané
- Výchozí nastavení systému
- Změna jazyka
- Systémový jazyk
+ Odběry
+ Systém
+ Jazyk
+ Systém
%1$s odběratelů
Nastavení
Zkusit znovu
@@ -70,50 +70,231 @@
Přizpůsobení
Webové stránky
%1$s videí
- Žádné připojení k internetu
+ Nejprve se připojte k internetu.
Komentáře
Výchozí karta
SponsorBlock
- Používá API z https://sponsor.ajay.app/
+ Používá API https://sponsor.ajay.app
Segment přeskočen
- Povolený
+ Zap
Segmenty
Sponzor
- Placená propagace, placené doporučení a přímá reklama. Nikoliv pro sebepropagaci nebo bezplatnnou zmíňku tvůrce/webové stránky/produktu, které se jim líbí.
+ Placená propagace, placená doporučení a přímé reklamy. Nezahrnuje sebepropagaci nebo shout-outy uživatelů/tvůrců/webů/produktů, které se tvůrcovi líbí.
Neplacená/vlastní propagace
- Podobné jako „sponzor“, s výjimkou neplacené nebo vlastní propagace. To zahrnuje sekce o vlastních produktech, darech nebo informacích o tom, s kým spolupracovali.
- Připomenutí interakce (přihlášení k odběru)
- Když se uprostřed obsahu objeví krátká připomínka, abyste je lajkovali, přihlásili se k odběru nebo sledovali. Pokud je dlouhý nebo se týká něčeho konkrétního, měl by být místo toho pod kategoriívlastní propagace.
+ Podobné jako „sponzor“, ale pro neplacenou nebo vlastní propagaci. Zahrnuje sekce o zboží, darech nebo informace o tom, s kým spolupracují.
+ Připomenutí interakce (like a odběr)
+ Krátká připomínka, abyste dali like, odběr nebo jste sledovali tvůrce uprostřed obsahu. Pokud je segment dlouhý nebo zahrnuje něco specifického, mělo by být místo toho zařazeno do vlastní propagace.
Přestávka/Intro
- Interval bez skutečného obsahu. Může to být pauza, statický snímek nebo opakující se animace. Nemělo by se používat pro přechody obsahující informace.
- Titulky
- Titulky nebo návrhy jiných videí na konci videa. Ne pro závěry s informacemi.
+ Interval bez skutečného obsahu. Může to být pauza, statický obrázek nebo opakující se animace. Nemělo by být použito pro přechody obsahující informace.
+ Koncové karty a titulky
+ Informace následující závěr. Není pro závěry s informacemi.
Kanály
Vše
Vyberte filtr hledání
- Seznamy videí
+ Playlisty
OK
Historie
Prohledat historii
- Vyčistit historii
+ Vymazat historii
Hudba YT Music
Videa YT Music
Alba YT Music
- Seznamy videí YT Music
+ Playlisty YT Music
Licence
- Pixel modrá
- Barevný akcent
- Červená
- Žlutá
- Zelená
- Fialová
- OLED motiv
- Material You
+ Blažená modrá
+ Barvy
+ Odpočinková červená
+ Neskutečně žlutá
+ Pěkná zelená
+ Příjemná fialová
+ Černý
+ Mystický Material 3
Upozornění
- Povoleno
- Zakázáno
- Ikona aplikace
+ Zap
+ Vyp
+ Ikona
Piped
YouTube
Přehrát na pozadí
+ Otevřít…
+ Obnovit výchozí
+ Obnovit všechna nastavení a odhlásit se\?
+ Kapitoly
+ Odstranit účet
+ Rychlost přehrávání
+ Je vyžadován restart aplikace
+ Zapněte Wi-Fi nebo mobilní data pro připojení se k internetu.
+ Restartovat aplikaci\?
+ Autoři
+ Poznejte všechny, kteří se podílejí na vývoji a vylepšování aplikace.
+ Stáhnout do
+ Kam se ukládají stahovaná média.
+ Interní úložiště
+ Karta SD
+ Hudební složka
+ Filmová složka
+ Sdílet URL s
+ %1$s zhlédnutí
+ Výchozí
+ Převod obou souborů při stažení zvuku i videa.
+ Pokud můžete, darujte tolik, kolik si myslíte že je hodnota této aplikace. Tým LibreTube je menší než váš příspěvek nebo pomoc.
+ Najít novou verzi
+ Klepněte pro zjištění, zda je aplikace aktuální.
+ Používáte nejnovější verzi.
+ Ztracená minulost
+ Používáte poslední verzi aplikace.
+ Název
+ Přechod Glib
+ Módní oheň
+ Název složky, do které se mají ukládat stahovaná média.
+ Složka pro stažené
+ Létající plamen
+ Posílený pták
+ Piped, přihlášení a odběry
+ Přidat…
+ V %1$s
+ Výplň/vtipy
+ Hudba: nehudební sekce
+ Náhled/shrnutí
+ Rychlost přehrávání
+ Tento komentář nemá žádné odpovědi.
+ Hloupý tvar
+ Název instance
+ Přidat instanci
+ Vymazat přidané
+ Seznamte se s týmem LibreTube a s tím, jak to všechno probíhá.
+ Zobrazit kapitoly
+ Skrýt kapitoly
+ Historie sledování
+ Pamatovat si pozici
+ Pokračovat z poslední pozice přehrávání
+ Formát videa pro přehrávač
+ Autentifikační instance
+ Použít jinou instanci pro autentifikační volání.
+ Vyberte autent. instanci
+ Auto
+ GitHub
+ Pro výplňové scény přidané jen jako přídavek nebo humor, které nejsou vyžadovány pro pochopení hlavního obsahu videa.
+ Použití pouze u hudebních videí. Mělo by pokrývat části videa, ne části oficiálních skladeb. Ve výsledku by mělo video připomínat verzi ze Spotify nebo jakoukoli jinou mixovanou verzi jak nejlépe je to možné, nebo omezit mluvení a další rušivé prvky.
+ Pro segmenty s informacemi o nadcházejícím obsahu nebo budoucích videích v dané sérii, neposkytuje ale další info. Pokud to zahrnuje klipy, které se zobrazí pouze zde, je to s největší pravděpodobností špatná kategorie.
+ URL API instance
+ Zadejte název a URL API.
+ Zadejte prosím adresu URL, která funguje
+ Související obsah
+ Zobrazit podobný obsah vedle toho, co sledujete.
+ Předběžné načítání
+ Maximální počet sekund videa ve vyrovnávací paměti.
+ Zvuk a video
+ Poměr stran videí
+ Orientace na celou obrazovku
+ Automatické otáčení
+ Na šířku
+ Na výšku
+ Twitter
+ Komunita
+ Discord
+ Matrix
+ Telegram
+ Reddit
+ Je dostupná verze %1$s
+ Přejít na vydání na GitHubu a stáhnout ji\?
+ Vzhled
+ Chování
+ Stahování
+ Formát videa
+ Navštivte web pro další informace o aplikaci a jejích funkcích.
+ Přispívání
+ Poskytujte nápady, překlady, změny vzhledu, čistěte a pište kód. Čím více se toho udělá, tím lépe!
+ Licence GPLv3+ je copyleftovaná svobodná licence. Používejte, studujte, měňte a sdílejte; se všemi.
+ Finančně přispět
+ Pokročilé
+ Přehrávač
+ Upravte si aplikaci podle svých představ.
+ Živě
+ Viditelnost navigační lišty
+ Vždy
+ Náhled videa
+ Zobrazit náhled při přesunu indikátoru přehrávání.
+ Kvalita
+ Titulky
+ Jazyk a oblast
+ Stahování a obnovení
+ Obnovit
+ Módní svítilna
+ Chování
+ Kvalita a chování přehrávače
+ Odstranit účet Piped
+ Žádný zvuk
+ Zvuk
+ Stahování…
+ Automatické přehrávání
+ URL frontendu instance
+ Žádné video
+ Video
+ Skrýt stránku s trendy
+ Ponechat historii sledování lokálně
+ Historie sledování a hledání
+ Zapamatované pozice přehrávání
+ Obnovit
+ Nainstalovat novou verzi LibreTube nyní\?
+ Obecné
+ Účet
+ Nikdy
+ Vybrané
+ Automaticky na celou obrazovku
+ Při otočení přepnout přehrávání na celou obrazovku.
+ Čistý motiv
+ Čistě bílý/černý motiv
+ Nebyl nalezen žádný externí přehrávač. Ujistěte se, že máte nějaký nainstalovaný.
+ Režim úspory dat
+ Přeskočit miniatury a další obrázky.
+ Pamatovat si vyhledávání
+ Systémový styl titulků
+ Žádné
+ Násobek hledání
+ Piped je svobodný alternativní webový frontend pro YouTube, který poskytuje API, které používáme. Bez něj by LibreTube neexistoval. Jejich vývojářům patří obrovský dík!
+ Automatické pozastavení
+ Pozastavit přehrávání při vypnutí obrazovky.
+ Automaticky přehrát další video po aktuálním.
+ Duplikovat playlist
+ Přehrávání na pozadí…
+ Stahování souboru APK…
+ Titulky
+ Formát zvuku pro přehrávač
+ Kvalita zvuku
+ Nejlepší
+ Nejhorší
+ Jazyk titulků
+ Upozornění
+ Upozornění na nová videa
+ Upozornění na nový obsah od tvůrců, které sledujete.
+ Kontrola každých…
+ Nová videa od %1$s…
+ Nejnovější
+ Nejstarší
+ Nejvíce zhlédnutí
+ Překlad
+ Nejméně zhlédnutí
+ Požadované připojení
+ Pomozte překládat aplikaci
+ Je k dispozici %1$s nových videí
+ Zatím žádná historie.
+ Název kanálu (A-Z)
+ Řazení
+ Pouze Wi-Fi
+ Opravdu\? Tohle se nedá vrátit!
+ Název kanálu (Z-A)
+ Vše
+ Měřené
+ Žádné výsledky.
+ Chyba
+ Zkopírováno
+ Stahování bylo úspěšné
+ Sdílet s počátečním časem
+ Exportovat odběry
+ Tlačítka přeskočení
+ Maximální velikost historie
+ Režim na pozadí
+ Zobrazit tlačítka k přeskočení na další nebo předchozí video.
+ Neomezená
\ No newline at end of file
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 3f8e751db..ab73570d4 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -3,183 +3,298 @@
Kvalitet
Bibliotek
Ophæv abonnement
- Password
+ Adgangskode
Log ind
- Registrer dig
+ Registrer
Log ud
- Udlogget.
- Allerede indlogget. Du kan logge ud av din konto når du vil.
- Log ind/registrer dig
+ Logged ud.
+ Allerede logged ind. Du kan logge ud af din konto når du vil.
+ Log ind/registrer
Log ind eller registrer dig i indstillingerne først.
- Nedlastning fuldført.
- Vent til alle nedlastninger er fuldført …
- Nedlastning mislykkets.
- Åbne i VLC
- Det er et problem med tjeneren. Prøv en anden instans\?
- Netværksfel.
- Du må skrive ind brugernavn og password.
- Forvalg for videoopløsning.
- Rudenetskolonner
+ Download fuldført.
+ Et andet download er allerede i gang, vent til alle downloads er færdige.
+ Download mislykkedes.
+ Åben i VLC
+ Der er et problem med serveren. Prøv en anden instans\?
+ Netværksfejl.
+ Du skal indtaste et brugernavn og en adgangskode.
+ Videoopløsning
+ Grid kolonner
Ingenting her.
- Opret afspilningsliste
- Slet afspilningslisten\?
- Afspilningsliste oprettet.
- Afspilningslistenavnet kan ikke være tomt.
- Leg til i afspilningsliste
- Lystig lys
- Svagsynet svart
+ Opret playliste
+ Slet playlisten\?
+ Playliste oprettet.
+ Playlistens navn må ikke være tom
+ Tilføj til playliste
+ Lyst
+ Mørkt
%1$s abonnementer
Indstillinger
- Sted
+ Lokation
Instans
- Website
+ Hjemmeside
%1$s videoer
- Koble til Internet først.
+ Opret forbindelse til internettet først.
Prøv igen
Kommentarer
Vælg søgefilter
Kanaler
Alle
- Spillelister
+ Playlister
OK
Historik
Søgehistorik
Ryd historik
- YT Musik-spor
- YT Musik-videoer
- YT Musik-album
- YT Musik-afspilningslister
- Standardfaneblad
+ YT Musik Sange
+ YT Musik Videoer
+ YT Musik Albummer
+ YT Musik Playlister
+ Standardfane
SponsorBlock
- Brug https://sponsor.ajay.app-API-et
- Undgået segment
+ Bruger https://sponsor.ajay.app API
+ Segment sprunget over
På
Segmenter
Sponsor
- Ligner «Sponsor», bortset fra at dette er ubetalt selv-promotion. Dette inkluderer segmenter om ting, donationer, eller info om samarbejdspartnere.
- Interval uden faktisk indhold. Kan være en pause, statisk ramme, gentagende animation. Skal ikke bruges for overganger som indeholder info.
+ Magen til \"Sponsor\", bortset fra ubetalt eller selvpromovering. Dette inkluderer segmenter om merchandise, donationer, eller info om samarbejdspartnere.
+ Et interval uden egentligt indhold. Kan være en pause, statisk billede, gentagende animation. Bør ikke bruges til overgange, der indeholder info.
Notifikationer
Ikon
YouTube
- Spil i bakgrunden
+ Spil i baggrunden
Version %1$s er tilgængelig
- Gå til udgivelser på GitHub for å laste ned\?
+ Gå til udgivelser på GitHub for at downloade det\?
Udseende
Adfærd
- Nedlastninger
- Lisens
- Sekundærfarve
+ Downloads
+ Licens
+ Accentfarve
Rolig rød
Belejlig blå
Videoformat
- Last ned til …
- Der nedlastede medier lagres.
- Bidrag
- Tilby idéer, oversættelser, designændringer, rens og skriv kode. Desto mer der bliver gjort, desto bedre bliver det.
- Konvertering af filer om både lyd og video er nedlastet.
- Gi det dette er værd for dig, hvis du kan. LibreTube-laget er mindre end din donation eller hjælp.
- Du kører den sidste versionen.
- Forvalg for afspilningshastighed
- Avancered
- Juster programmet til din smag.
+ Download til
+ Hvor downloadede medier bliver gemt.
+ Bidrager
+ Foreslå idéer, oversættelser, designændringer, rengør og skriv kode. Desto mere der bliver gjort, desto bedre bliver det!
+ Konvertering af filer hvis både lyd og video er downloadet.
+ Giv hvad det er værd for dig, hvis du kan. Team LibreTube er mindre end din donation eller hjælp.
+ Du kører den seneste version.
+ Afspilningshastighed
+ Avanceret
+ Tilpas appen efter din smag.
Direkte
- Ubesvaret kommentar.
- Udviklere
+ Denne kommentar har ingen svar.
+ Forfattere
Navn
- Internlager
- Bliv kend med LibreTube-laget og hvordan alt sker.
- Navnet på mappen nedlastede medier lagres i.
- Filmmappe
- Del URL til …
- Forvalg
- Nedlastningsmappe
- Musikmappe
- SD-kort
+ Internt lager
+ Lær alle at kende, der er involveret i at udvikle og forbedre appen.
+ Navnet på mappen downloadede medier er gemt i.
+ Film mappe
+ Del URL til
+ Standard
+ Download mappe
+ Musik mappe
+ SD kort
Fashionabel flamme
- Fiktiv fakkel
- Formodentlig formet
- Flyende flamme
- Fremmed fugl
+ Trendy fakkel
+ Dumt formet
+ Flyvende flamme
+ Forstærket fugl
Hjem
Abonnementer
- Vælg en instans
- Dragt
+ Vælg…
+ Tema
Ja
Søg
- Lagre
+ Gem
Videoer
Abonner
- Indlogget.
- Vælg en region
+ Logged ind.
+ Region
Del
Brugernavn
- Der gik nogenting galt.
- Last ned
+ Noget gik galt.
+ Download
Afbryd
- Registrered. Du kan nu abonnere på kanaler.
+ Registreret. Du kan nu abonnere på kanaler.
Log ind og prøv igen.
Abonneret
- Leg til egendefineret instans
+ Brugerdefineret
Abonner på nogle kanaler først.
- Kan ikke laste ned denne strømmen.
- Kan ikke åbne i VLC. Måske det ikke er installeret\?
- Importer abonnementer
+ Kan ikke downloade denne stream.
+ Kan ikke åbnes i VLC. Det er muligvis ikke installeret.
+ Importér abonnementer
Fra YouTube eller NewPipe
- Dette er for en LibreTube-konto.
- Slet afspilningslisten
+ Dette er for en Piped-konto.
+ Slet playliste
Færdig.
- Se efter ny version
- Afspilningslistenavn
- Mislykket :(
+ Led efter ny version
+ Playlistenavn
+ Mislykkedes :(
Om
Sprog
System
Af
System
Justeringer
- Svagsynet svart
+ Sort
På
Lemfældig lilla
Mystisk Materiel 3
- Klik for at finne ud om programmet er av nyeste dato.
+ Klik for at finde ud af, om appen er opdateret.
Piped
- Kører sidste version.
- Doner
+ Kører den seneste version.
+ Donér
Afspiller
- Afspiller, nedlastninger, historik
+ Downloads og nulstil
%1$s visninger
Glidende gradient
- God gammeldags
+ Tabt arv
Gallisk gul
- Gestende grøn
- Besøg vores website for mer info om programmet og dets funktioner.
- Betalt promotion, betalte henvisninger, og direkte reklame. Ikke for selv-promotion, eller gratis nævning av tiltag/skabere/websites/produkter.
- Interaktionspåmindelse (lig og abonner)
- Ubetalt/selv-promotion
- Pausesegment/introanimation
- Korte påmindelser om å lige, abonnere, eller følge midt i indhold. Hvis det er langt eller specifikt skal det gå som selv-promotion.
+ Gesten grøn
+ Besøg hjemmesiden for mere information om appen og dens funktioner.
+ Betalt promovering, betalte henvisninger, og direkte reklamer. Ikke for selvpromovering, eller reelle shoutouts af tiltag, kreatører, hjemmesider og produkter.
+ Interaktionspåmindelse (synes godt om og abonner)
+ Ubetalt/Selvpromovering
+ Afbrydelse/Introanimation
+ Når der er en kort påmindelse om at synes godt om, abonnere eller følge midt i indhold. Hvis det er langt eller specifikt, skal det i stedet gå som selvpromovering.
Slutinfo og rulletekst
- Info som kommer efter slutten af videoen. Ikke for konklusioner med info.
- GPLv3+ er en gemenfrihedslig lisens. Brug, studer, ændre og del; med alle.
- Piped, indlogging, abonnementer
- Læg til en egendefineret instans (på egen risiko)
- Navn på instans
- URL til instansens API
- Læg til instans
- Tøm egendefinerede instanser
- Skriv ind en URL der virker
- Relaterede strømmer
- Version %1$s
- Bliv kendt med LibreTube-laget og hvordan det hele går til.
- Vis videoer relateret til det du ser.
- Du må fylde ind navnet og API-URL.
+ Info som kommer efter videons afslutning. Ikke for konklusioner med info.
+ GPLv3+ er en copylefted fri licens. Brug, studér, ændre og del; med alle.
+ Piped, login og abonnementer
+ Tilføj…
+ Instansnavn
+ URL til API instans
+ Tilføj instans
+ Ryd tilføjet
+ Indtast venligst en URL, der virker
+ Relateret indhold
+ V %1$s
+ Lær team LibreTube at kende, og hvordan det hele foregår.
+ Vis lignende streams ved siden af det, du ser.
+ Udfyld navnet og API-URL\'en.
Vis kapitler
- Skjul kaptitler
- Fyld-udledelser/vitser
- For segmenter som viser vad som følger i denne eller kommende videoer i serien, men ikke har yderligere info. Hvis dette inkluderer klip som kun vises her er dette antagelig ikke rigtig kategori.
- Mellemlagringsmål
- Videoformat for afspiller
- Kun for brug i musikvideoer. Skal bruges for deler av videoen som ikke er en del av den officielle miksen. Til sidst skal videoen tilsvare den på Spotify, eller en anden mikset version så nært som mugligt, eller reducere snakk eller andre distraktioner.
- Musik: Parti uden musik
- Forhåndsvisning/tilbageblik
+ Skjul kapitler
+ Fylde Indhold/Jokes
+ For segmenter som beskriver kommende indhold i videoen eller kommende videoer i serien, men ikke har yderligere info. Hvis dette inkluderer klip som kun vises her, er dette højst sandsynligt ikke den rigtige kategori.
+ Forudindlæsning
+ Videoformat til afspiller
+ Kun til brug i musikvideoer. Skal bruges til dele af videoen som ikke er en del af det officielle mix. Til sidst skal videoen svare til den på Spotify, eller anden mixet version så nært som mugligt, eller mindske snak eller andre distraktioner.
+ Musik: Sektioner uden musik
+ Forhåndsvisning/Opsummering
+ Åben…
+ Undertekstsprog
+ Søge stigning
+ Automatisk pause
+ Sæt afspilning på pause, når skærmen er slukket.
+ Nulstil alle indstillinger og log ud\?
+ Slet din Piped-konto
+ Gendan standardindstillingerne
+ Konto
+ Kapitler
+ Slet konto
+ Gendan
+ Afspilningshastighed
+ Genstart appen\?
+ App genstart påkrævet
+ Slå Wi-Fi eller mobildata til for at oprette forbindelse til internettet.
+ Se historik
+ Husk position
+ Godkendelsesinstans
+ Brug en anden instans til godkendte opkald.
+ Vælg en godkendelsesinstans
+ Auto
+ GitHub
+ Til tangentielle scener tilføjet kun til fyldstof eller humor, der ikke er påkrævet for at forstå videoens hovedindhold.
+ Maks. antal sekunder af video som buffer.
+ Lyd og video
+ Fuld skærm orientering
+ Størrelsesforhold for videoer
+ Automatisk rotation
+ Landskab
+ Portræt
+ Twitter
+ Fællesskab
+ Discord
+ Matrix
+ Telegram
+ Reddit
+ Downloader…
+ Ingen lyd
+ Ingen video
+ Lyd
+ Video
+ Automatisk afspilning
+ Navigationsbar etiket synlighed
+ Altid
+ Valgte
+ Aldrig
+ Auto fuldskærm
+ Fortsæt fra sidste afspilningsposition
+ Afspilning i fuld skærm, når enheden vendes.
+ Rent tema
+ Rent sort/hvid tema
+ Ingen ekstern afspiller fundet. Sørg for, at du har en installeret.
+ Databesparende tilstand
+ Spring over thumbnails og andre billeder.
+ Husk søgninger
+ Hold styr på sete videoer lokalt
+ Se- og søgehistorik
+ Huskede afspilningspositioner
+ Nulstil
+ Systemtekst udssende
+ Undertekster
+ Ingen
+ Installér den nye LibreTube-version nu\?
+ Generel
+ Sprog og region
+ Video forhåndsvisning
+ Vis et øjebliksbillede, når du trækker i afspilningsindikatoren.
+ Skjul trendside
+ URL til frontendinstans
+ Kvalitet
+ Adfærd
+ Kvalitet og afspilleradfærd
+ Piped er en fri alternativ web-frontend til YouTube, der tilbyder den API, vi bruger. Uden det ville LibreTube ikke eksistere. Stor tak til deres udviklere!
+ Afspil den næste video automatisk efter den aktuelle.
+ Afspiller i baggrunden…
+ Klon playliste
+ Undertekster
+ Downloader APK…
+ Lydformat til afspiller
+ Lydkvalitet
+ Bedst
+ Værst
+ Nye streams fra %1$s…
+ Ingen historik endnu.
+ Færreste visninger
+ Kanalnavn (A-Å)
+ Sortering
+ Tjekker hver …
+ Nyeste
+ Notifikationer
+ Notifikationer for nye streams
+ Notifikationer om nyt indhold fra kanaler, du følger.
+ Er du sikker\? Dette kan ikke fortrydes!
+ Ældste
+ %1$s nye streams tilgængelige
+ Fleste visninger
+ Kanalnavn (Å-A)
+ Alle
+ Forbrugsbaseret
+ Oversættelse
+ Hjælp med at oversætte appen
+ Ingen resultater.
+ Kun på Wi-Fi
+ Påkrævet forbindelse
+ Fejl
+ Kopieret
+ Del med starttidspunkt
+ Download lykkedes
+ Eksportér abonnementer
+ Spring over knapper
+ Vis knapper for at springe til næste eller forrige video.
+ Ubegrænset
+ Maksimal historiestørrelse
+ Baggrundstilstand
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index fb8a9188e..1b81cd595 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -17,8 +17,8 @@
Abgemeldet.
Registrierung erfolgreich! Du kannst nun Kanäle abonnieren.
Bitte melde dich an und versuche es nochmal.
- Instanz wählen
- Benutzerdefinierte Instanz
+ Instanz wählen…
+ Benutzerdefiniert
Region
Abonniert
Abonniere erst einige Kanäle.
@@ -43,7 +43,7 @@
Name der Wiedergabeliste
Zu Wiedergabeliste hinzufügen
Fehlgeschlagen :(
- Qualtität:
+ Qualtität
Anmelden/registrieren
Herunterladen abgeschlossen.
Speichern
@@ -103,11 +103,11 @@
Lizenz
Akzente
Ruhendes Rot
- Pixelblau
+ Glückseliges Blau
Gelb
Groovy grün
Angenehmes Lila
- Schwarzes Farbschema
+ Schwarz
Mystic Material 3
Benachrichtigungen
Ein
@@ -118,7 +118,7 @@
Im Hintergrund abspielen
Version %1$s ist verfügbar
Gehe zu den Veröffentlichungen auf GitHub, um sie herunterzuladen\?
- Player, Herunterladen, Verlauf
+ Herunterladen, Zurücksetzen
Aussehen
Verhalten
Datenübertagungen
@@ -142,47 +142,47 @@
Live
Dieser Kommentar hat keine Antworten.
Autoren
- Lernen Sie das Team LibreTube und all seine Mitwirkenden kennen, die helfen, die Anwendung zu verbessern.
+ Lernen Sie alle Mitwirkenden, welche in der Entwicklung und der Verbesserung der Anwendung involviert sind, kennen.
Beitragen
Name
Der Name des Ordners, in dem die heruntergeladenen Medien gespeichert sind.
SD-Karte
Interner Speicher
Musikordner
- URL teilen an:
+ URL teilen an
Download-Ordner
Filmordner
%1$s Aufrufe
Standard
Instanzname
- Instanz-API-URL
+ URL zur Instanz-API
Instanz hinzufügen
- Du musst den Namen und die API-URL eingeben.
+ Geben Sie den Namen und die API URL ein.
Benutzerdefinierte Instanzen löschen
- Bitte gib eine gültige URL ein
- Piped, Anmeldung, Abonnements
- Hinzufügen einer benutzerdefinierten Instanz (auf eigenes Risiko)
+ Bitte geben Sie eine gültige URL ein
+ Piped, Anmeldung & Abonnements
+ Hinzufügen…
Musik: Nicht-musikalische Abteilung
Vorschau/Rückblick
Kapitel ausblenden
Kapitel anzeigen
Wird vorgeladen
- Die Anzahl der Sekunden, die Videos maximal vorgeladen werden.
+ Maximal Zahl an Sekunden die vorausgeladen werden.
Nur zur Verwendung in Musikvideos. Es sollte Teile des Videos abdecken, die nicht Teil der offiziellen Abmischungen sind. Am Ende sollte das Video der Spotify- oder einer anderen abgemischten Version so nahe wie möglich kommen oder das Sprechen oder andere Ablenkungen reduzieren.
Videoformat für Player
Version %1$s
Lernen Sie das Team LibreTube kennen und erfahren Sie, wie das alles abläuft.
Für Segmente, die auf kommende Inhalte in diesem oder zukünftigen Videos der Serie hinweisen, aber keine zusätzlichen Informationen liefern. Wenn es Clips enthält, die nur hier erscheinen, ist dies sehr wahrscheinlich die falsche Kategorie.
Automatische Wiedergabe
- Zeigen Sie verwandte Streams neben dem, was Sie sehen.
+ Zeigen Sie ähnliche Streams neben dem, was Sie sehen.
Verwandte Inhalte
Kein Ton
Kein Video
Audio
Video
- Wird heruntergeladen
+ Wird heruntergeladen…
Standardwerte wiederherstellen
- Sind Sie sicher\? Dadurch werden Sie abgemeldet und alle Ihre Einstellungen zurückgesetzt!
+ Alle Einstellungen zurücksetzen und abmelden\?
Konto löschen
Ihr Piped-Konto löschen
Konto
@@ -195,7 +195,7 @@
Wiedergabeliste klonen
Wiedergabeverlauf
URL zum Instanz-Frontend
- Piped ist ein alternatives quelloffenes Web-Frontend für YouTube, das die von uns verwendete API bereitstellt. Ohne Piped, würde LibreTube nicht existieren. Großer Dank an ihre Entwickler!
+ Piped ist ein alternatives quelloffenes Web-Frontend für YouTube, welches die von uns verwendete API bereitstellt. Ohne Piped würde LibreTube nicht existieren. Großen Dank an die Entwickler!
Den Player pausieren, wenn der Bildschirm ausgeschaltet ist.
Das nächste Video automatisch abspielen, wenn das aktuelle beendet ist.
Position merken
@@ -204,4 +204,56 @@
Wählen Sie eine Autorisierungsinstanz
GitHub
Auto
+ Bitte stellen Sie eine Internetverbindung her, indem Sie WLAN oder mobile Daten aktivieren.
+ Öffnen…
+ Kapitel
+ Wiedergabegeschwindigkeit
+ Neustart der App erforderlich
+ Möchten Sie die App jetzt neu starten\?
+ Audio und Video
+ Ausrichtung im Vollbildmodus
+ Video-Bildverhältnis
+ Automatische Drehung
+ Landschaft
+ Porträt
+ Gemeinschaft
+ Discord
+ Matrix
+ Telegram
+ Reddit
+ Twitter
+ Verlorenes Vermächtnis
+ Füllungstangente/Witze
+ Keine
+ Wollen Sie die Anwendung jetzt aktualisieren\?
+ Für tangentiale Szenen, die nur als Füllmaterial oder Humor hinzugefügt wurden, müssen nicht den Hauptinhalt des Videos verstehen.
+ Ausgewählt
+ Automatischer Vollbildmodus
+ Nie
+ Vollbild-Modus des Players wird aktiviert, wenn das Gerät gedreht wird.
+ Kein externer Player gefunden. Bitte stellen Sie sicher, dass Sie einen installiert haben.
+ Modisches Feuer
+ Trendige Fackel
+ Albern geformt
+ Fliegende Flamme
+ Verstärkter Vogel
+ Immer
+ Reines Farbschema
+ Reines weißes/schwarzes Farbschema
+ Datensparmodus
+ Keine Miniaturansichten und andere Bilder laden, um Daten zu sparen.
+ Suchanfragen lokal speichern
+ Benachrichtigungen
+ Überprüfe alle …
+ Zurücksetzen
+ Allgemein
+ Sprache und Region
+ Beste Qualität
+ Video-Vorschau
+ Von letzter Position weiter abspielen
+ Navigationsleistensichtbarkeit
+ Angesehene Videos lokal speichern
+ Wiedergabe- und Suchverlauf
+ Untertitel
+ Vorspul-Länge
\ 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 6d8d7bc51..6d3384335 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -154,7 +154,7 @@
La GPLv3+ es una licencia libre con copyleft. Utiliza, estudia, cambia y comparte; con todos.
Carpeta de películas
Estás utilizando la última versión.
- Descargas, historial
+ Descargas, restablecimiento
Antiguo
El nombre de la carpeta en la que se almacenan los medios descargados.
Añadir instancia
@@ -186,7 +186,7 @@
Sólo para su uso en vídeos musicales. Debe abarcar partes del vídeo que no formen parte de las mezclas oficiales. Al final, el vídeo debe parecerse lo más posible a la versión de Spotify o a cualquier otra versión mezclada, o reducir las conversaciones u otras distracciones.
Reproducción automática
Sin vídeo
- Descargando
+ Descargando…
Ocultar página de tendencias
URL de la interfaz de la instancia
Sin audio
@@ -209,7 +209,7 @@
Eliminar cuenta
Historial de visualizaciones
Recordar posición
- Recuerda la posición de visualización y se desplaza automáticamente hacia ella.
+ Restablece la última posición vista
Instancia de autenticación
Utilice una instancia diferente para las llamadas autenticadas.
Elija una instancia de autenticación
@@ -227,7 +227,31 @@
Comunidad
Discord
Twitter
- Por favor, conéctese a Internet activando el WiFi o los datos móviles.
+ Por favor, conéctese a internet activando el wifi o los datos móviles.
Abrir…
Capítulos
+ Es posible que estos cambios no se apliquen sin reiniciar la aplicación. ¿Quieres reiniciar la aplicación ahora\?
+ Velocidad de reproducción
+ Es necesario reiniciar
+ Seleccionado
+ Las consultas de búsqueda se almacenan localmente
+ Restablecer posiciones vistas
+ Visibilidad de la etiqueta de la barra de navegación
+ Siempre
+ Nunca
+ Pantalla completa automática
+ Cambia automáticamente a pantalla completa del reproductor cuando se gira el dispositivo.
+ Tema puro
+ Tema blanco/negro puro
+ No se ha encontrado ningún reproductor externo. Por favor, asegúrese de que tiene uno instalado.
+ Modo ahorro de datos
+ Las miniaturas y otras imágenes no se cargan para ahorrar datos.
+ Lleva el control de los vídeos vistos localmente
+ Historial de búsqueda y visualización
+ Posiciones vistas
+ Estilo de subtítulos del sistema
+ Subtítulos
+ Ninguno
+ ¿Quiere actualizar la aplicación ahora\?
+ Vista previa de la barra de desplazamiento
\ No newline at end of file
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index c272f21fb..f756c990f 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -20,14 +20,14 @@
Erregistratuta. Orain kanaletan harpidetu zaitezke.
Saioa hasia duzu. Saioa itxi dezakezu.
Saioa hasi eta saiatu berriro mesedez.
- Hautatu instantzia
- Instantzia pertsonalizatua
+ Hautatu…
+ Pertsonalizatua
Lurraldea
Saioa hasi/erregistratu
Lehenengo ezarpenetan saioa hasi edo erregistratu mesedez.
Harpidetuta
Lehenengo kanalen batean harpidetu mesedez.
- Ezin da deskargatu jario hau.
+ Ezin da deskargatu korronte hau.
Deskargatuta.
Beste deskarga bat abian dago, itxaron bukatu arte mesedez.
Ezin izan da deskargatu.
@@ -40,7 +40,7 @@
Sareko akatsa.
Zerbait ez dabil ondo.
Hau Piped kontu baterako da.
- Lehenetsitako bideo bereizmena
+ Bideo bereizmena
Lauki-sare zutabea
Ezer hemen.
Ezabatu erreprodukzio zerrenda
@@ -65,7 +65,7 @@
Iruzkinak
Kanalak
Ados
- Ezabatu historiala
+ Ezabatu historia
YT Musika kantak
YT Musika bideoak
YT Musika albumak
@@ -109,7 +109,7 @@
Aurreratua
Erreproduzitzailea
Egokitu aplikazioa zure gustura
- Deskargak, historiala
+ Deskargak eta berrezartzea
Iruzkin honek ez du erantzunik.
Ezagutu aplikazioaren garapenean eta hobekuntzan parte hartzen duten guztiak.
Izena
@@ -139,8 +139,8 @@
Sistema
Saiatu berriro
Dena
- Historiala
- Bilatu historiala
+ Historia
+ Bilaketen historia
Erreprodukzio zerrendak
Babeslea
Alde batera utzitako zatiak
@@ -158,21 +158,21 @@
Dohaintza
Klik egin aplikazioa eguneratuta dagoen jakiteko.
Zuzenean
- Erreprodukzio-abiadura lehenetsia
+ Erreprodukzio-abiadura
Egileak
Barne biltegiratzea
- Gehitu instantzia pertsonalizatua (zure ardurapean)
+ Gehitu…
Intantziaren API URL-a
Izena eta API URL-a bete.
Mesedez, sartu badabilen URL bat
Instantziaren izena
Gehitu instantzia
- Ezabatu instantzia pertsonalizatuak
- Piped, saioa hasi, harpidetzak
+ Ezabatu gehituak
+ Piped, saio hasiera eta harpidetzak
%1$s bertsioa
Ezagutu LibreTubeko taldea eta nola gertatzen den dena.
Erlazionatutako edukia
- Erakutsi ikusten duzunarekin erlazionatutako fluxuak.
+ Erakutsi ikusten duzunaren antzeko korronteak.
Erakutsi kapituluak
Ezkutatu kapituluak
Musika: Musikarik gabeko atala
@@ -189,16 +189,16 @@
Audiorik gabe
Audioa
Bideoa
- Deskargatzen
+ Deskargatzen…
Ezkutatu joera-orria
- Instantziaren frontenderako URLa
+ Instantziaren frontend-erako URL-a
Kalitatea
Portaera
Kalitatea eta erreprodukzioaen portaera
- Bilatu gehikuntza
- Piped kode irekiko web-frontend alternatibo bat da eta YouTube-k erabiltzen duen APIa eskaintzen du. Piped gabe, LibreTube ez litzateke existituko. Mila esker haien garatzaileei!
+ Joan-etorrien gehikuntza
+ Piped web-frontend alternatibo libre bat da eta YouTube-k erabiltzen duen APIa eskaintzen du. Piped gabe, LibreTube ez litzateke existituko. Mila esker haien garatzaileei!
Leheneratu lehenetsiak
- Ziur zaude\? Honek saioa amaitu eta zure ezarpen guztiak berrezarriko ditu!
+ Ziur al zaude saioa amaitu eta zure ezarpen guztiak berrezarri nahi dituzula\?
Pantaila itzalita dagoenean, erreproduzitzailea gelditu.
Erreproduzitu automatikoki hurrengo bideoa unekoa amaitzen denean.
Automatikoki gelditzea
@@ -207,12 +207,12 @@
Ezabatu zure Piped kontua
Kontua
Berreskuratu
- Ikusi historiala
+ Ikusitakoen historia
Gogoratu posizioa
- Erreprodukzioaren kokapena gogoratu eta bertara joan automatikoki.
- Egiaztatze deietarako erabili beste instantzia bat.
- Egiaztatze instantzia
- Hautatu egiaztatze instantzia
+ Jarraitu azken erreprodukzioaren kokapenean
+ Autentifikaziorako erabili beste instantzia bat.
+ Autentifikaziorako instantzia
+ Hautatu autentifikaziorako instantzia
Automatikoa
GitHub
Audioa eta bideoa
@@ -227,4 +227,68 @@
Reddit
Telegram
Twitter
+ Ireki …
+ Kapituluak
+ Erreprodukzio abiadura
+ Berrabiaraztea behar da
+ Mesedez, konekta zaitez internetera.
+ Aldaketa hauek agian ez dira aplikatuko aplikazioa berrabiarazi arte. Aplikazioa berrabiarazi nahi duzu\?
+ Nabigazio-barrako etiketen ikusgarritasuna
+ Beti
+ Hautatuta
+ Inoiz ez
+ Pantaila osoa automatikoa
+ Aldatu automatikoki erreproduzitzailearen pantaila osora gailua pizten denean.
+ Gai purua
+ Gai zuri/beltz purua
+ Ez da kanpoko erreproduzitzailerik aurkitu. Mesedez, ziurtatu bat instalatuta duzula.
+ Datuak aurrezteko modua
+ Ez kargatu miniaturarik, ez bestelako irudirik datuak aurrezteko.
+ Gailuan biltegiratu bilaketa-kontsultak
+ Bistaratze eta bilaketaren historia
+ Berrezarri bistaratzeen posizioak
+ Sistemaren azpitituluen estiloa
+ Bat ere ez
+ Aplikazioa eguneratu nahi duzu orain\?
+ Bilaketa barrako aurrebista
+ Aurreikusi bideoa bilaketa-barra garbitzean posizioa bilatuz.
+ Gailuan ikusitako bideoen jarraipena egitea
+ Bistaratzeen posizioak
+ Azpitituluak
+ Kalitaterik onena
+ Kalitaterik txarrena
+ Azpitituluen hizkuntza lehenetsia
+ Erreproduzitzailearen audio formatua
+ Audioaren kalitatea
+ Orokorra
+ Hizkuntza, eskualdea
+ Atzeko planoan erreproduzitzen…
+ Azpitituluen ezarpenak
+ Apk deskargatzen …
+ Korronte berrien jakinarazpenak
+ Maiztasuna egiaztatzea
+ %1$s korronte berri daude eskuragarri
+ %1$s-ren korronte berriak…
+ Jakinarazpenak
+ Historia hutsik dago.
+ Ziur al zaude\? Hau ezin da desegin!
+ Jakinarazi harpidetzetako korronte berriak daudenean
+ Berriena
+ Gutxien ikusiak
+ Ordenatu
+ Berrienak
+ Ikusienak
+ Kanalaren izena (A-Z)
+ Kanalaren izena (Z-A)
+ Neurtua
+ WiFi bakarrik
+ Ez da emaitzarik aurkitu.
+ Beharrezko sare mota
+ Sare guztiak
+ Itzuli
+ Lagundu aplikazioa hitz egiten duzun hizkuntzara itzultzen
+ Kopiatuta
+ Deskarga egina
+ Partekatu denbora kodearekin
+ Akatsa gertatu da
\ 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 8daded198..4fd170109 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -121,7 +121,7 @@
Si vous aimez l\'application et appréciez notre travail, nous serions heureux de votre don.
Contribution
Faire un don
- Téléchargements, historique
+ Téléchargements, réinitialisation
Conversion des fichiers si on télécharge à la fois de l\'audio et de la vidéo.
Où sont stockés les médias téléchargés.
La GPLv3+ est une licence libre copylefté. Utilisez, étudiez, modifiez et partagez ; avec tous.
@@ -189,7 +189,7 @@
Sans vidéo
Audio
Vidéo
- Téléchargement
+ Téléchargement…
Masquer la page des tendances
URL vers l\'interface de l\'instance
Qualité
@@ -209,7 +209,7 @@
Restaurer
Regarder l’historique
Se souvenir de la position
- Se souvenir de l\'endroit que vous regardiez et le chercher automatiquement.
+ Rétablir la dernière position de visionnage
Instance d\'authentification
Auto
GitHub
@@ -228,6 +228,63 @@
Communauté
Discord
Ouvrir …
- Veuillez vous connecter à l\'internet en activant le WiFi ou les données mobiles.
+ Veuillez vous connecter à internet en activant le Wi-Fi ou les données mobiles.
Chapitres
+ Vitesse de lecture
+ Ces changements peuvent ne pas être appliqués sans redémarrer l\'application. Voulez-vous redémarrer l\'application maintenant \?
+ Redémarrage nécessaire
+ Visibilité de l\'étiquette de la barre de navigation
+ Toujours
+ Plein écran automatique
+ Passage automatique en plein écran du lecteur lorsque l\'appareil est retourné.
+ Thème pur
+ Aucun lecteur externe n\'a été trouvé. Veuillez vous assurer que vous en avez installé un.
+ Mode économie de données
+ Stocker les requêtes de recherche localement
+ Garder la trace des vidéos regardées localement
+ Historique de visionnage et de recherche
+ Positions de visionnage
+ Réinitialiser les positions de visionnage
+ Style des légendes du système
+ Légendes
+ Aucun
+ Sélectionné
+ Ne pas charger les miniatures et autres images pour économiser des données.
+ Voulez-vous mettre à jour l\'application maintenant \?
+ Jamais
+ Thème blanc/noir pur
+ Aperçu de la barre de recherche
+ Prévisualisez la vidéo en recherchant la position lors de la scrutation de la barre de recherche.
+ Langue, région
+ Général
+ Qualité audio
+ Meilleure qualité
+ Pire qualité
+ Lecture en arrière-plan…
+ Paramètres des légendes
+ Téléchargement de l\'APK…
+ Format audio pour le lecteur
+ %1$s nouveaux flux sont disponibles
+ Langue des sous-titres par défaut
+ Notifications
+ Tri
+ Plus anciens
+ Plus vus
+ Moins vus
+ Traduire
+ Aidez-nous en traduisant l\'application dans la langue que vous parlez
+ Plus récents
+ Tous les réseaux
+ Nom de chaine (Z-A)
+ WiFi uniquement
+ Nom de chaine (A-Z)
+ Êtes-vous sûr(e) \? Ça ne peut pas être annulé !
+ L\'historique est vide.
+ Nouvelles notifications de flux
+ Notifie quand de nouveaux flux des souscriptions sont disponibles
+ Mesure
+ Aucun résultat trouvé.
+ Nouveaux stream par %1$s…
+ Type de réseau nécessaire
+ Fréquence de vérification
\ No newline at end of file
diff --git a/app/src/main/res/values-hy/strings.xml b/app/src/main/res/values-hy/strings.xml
new file mode 100644
index 000000000..b34359c25
--- /dev/null
+++ b/app/src/main/res/values-hy/strings.xml
@@ -0,0 +1,54 @@
+
+
+ Դարան
+ Որակ
+ Տեսանյութեր
+ Հետևել
+ Տարածել
+ Պահպանել
+ Մուտք գործել
+ Գրանցվել
+ Դուրս գալ
+ Չեղարկել
+ Դուրս եկած։
+ Ընտրել․․․
+ Թեմա
+ Հնարավոր չէ ներբեռնել:
+ Ներբռնումն ավարտվեց:
+ Ներբեռնել չհաջողվեց:
+ Բացել VLC-ում
+ Չհաջողվեց բացել VLC-ին: Հնարավոր է այն տեղադրված չէ:
+ Ներմուծել բաժանորդագրություններ
+ YouTube-ից կամ NewPipe-ից
+ Սերվերի հետ խնդիր կա: Փորձե՞լ այլ նմուշ:
+ Ցանցի սխալ:
+ Տեսանյութի չափը
+ Ցանց սյունակներ
+ Այստեղ ոչինչ չկա:
+ Ջնջեել երգացանկը
+ Մասին
+ Լեզու
+ Համակարգ
+ Համակարգ
+ Բաց
+ Մուգ
+ %1$s հետևորդ
+ Կարգվորումներ
+ Գտնվելու վայրը
+ Նմուշ
+ Կայք
+ %1$s տեսանյութ
+ Սկզբից միացեք համացանցին:
+ Գլխավոր
+ Այո
+ Չհետևել
+ Ներբեռնել
+ Որոնել
+ Մուտք գործած։
+ Տարածաշրջան
+ Օգտանուն
+ Գաղտնաբառ
+ Մեկ այլ ներբեռնում արդեն ընթացքի մեջ է, սպասեք մինչ այն ավարտվի:
+ Ինչ-որ բան սխալ է:
+ Սա Piped հաշվի համար է:
+
\ No newline at end of file
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 203d50ab4..38e7f1723 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -2,7 +2,7 @@
Ya
Berhenti Berlangganan
- Pilih Kualitas:
+ Kualitas
Telusuri
Berlangganan
Unduh
@@ -23,25 +23,25 @@
Impor langganan
Ini bukan seperti akun google Anda.
Anda sudah masuk, Anda dapat keluar dari akun anda.
- Silahkan login dan coba lagi!
+ Silahkan masuk dan coba lagi.
Pilih instance
Tambahkan instance kustom
- Pilih lokasi
- Masuk/Daftar
+ Lokasi
+ Masuk/daftar
Berlangganan ke beberapa saluran terlebih dahulu
Tidak dapat mengunduh, ini adalah aliran.
Unduhan selesai.
- Unduhan yang lain sedang berlangsung. Silahkan tunggu sampai selesai.
+ Unduhan yang lain sedang berlangsung, silahkan tunggu sampai selesai.
Buka di VLC
- Daftar putar dibuat!
+ Daftar putar dibuat.
Silakan masuk atau buat akun di setelan terlebih dahulu
Berlangganan
- Petak Kolom
+ Petak kolom
Belum ada konten apapun disini
Hapus daftar putar
Anda yakin ingin menghapus daftar putar\?
Buat daftar putar
- Nama Daftar Putar
+ Nama daftar putar
Dari NewPipe atau Youtube
Ada masalah dengan server. Cobalah instance lain\?
Terjadi kesalahan jaringan.
@@ -53,14 +53,14 @@
Koleksi
Tema
Bahasa
- Bawaan Sistem
- Bahasa Sistem
- Tema Terang
- Tema Gelap
- Berhasil!
+ Sistem
+ Sistem
+ Terang
+ Gelap
+ Berhasil.
Gagal :(
Tentang
- Tambahkan ke Daftar Putar
+ Tambahkan ke daftar putar
Nama daftar putar wajib diisi
%1$s langganan
Setelan
@@ -69,7 +69,7 @@
Kustomisasi
Situs Web
%1$s video
- Tidak ada koneksi internet
+ Hubungkan ke internet terlebih dahulu
Coba lagi
Komentar
filter penelusuran
@@ -82,7 +82,7 @@
Hapus riwayat penelusuran
YT Musik Lagu
YT Musik Video
- Aktifkan
+ Aktif
YT Musik Album
YT Musik Daftar Putar
Tab Bawaan
@@ -91,17 +91,17 @@
Lewati segmen
Sponsor
Segmen
- Promosi dibayar, tautan dibayar dan iklan langsung. Tidak untuk promosi diri sendiri atau dukungan gratis untuk gerakan/kreator/situs/produk yang mereka suka.
+ Promosi dibayar, tautan dibayar dan iklan langsung. Tidak untuk promosi diri sendiri atau dukungan gratis untuk gerakan, kreator, situs.
Promosi Diri Sendiri/Tidak Dibayar
Ikon Apl
Youtube
Mirip dengan \"sponsor\" kecuali ini tidak dibayar atau promosi diri sendiri. Ini termasuk merchandise, donasi, atau informasi tentang siapa yang berkolaborasi dengan mereka.
- Pengingat Interaksi (Berlangganan)
+ Pengingat interaksi (suka dan berlangganan)
Jeda/Animasi Intro
Bagian yang bukan konten sebenarnya. Dapat berupa jeda, gambar statik, atau animasi berulang. Ini tidak boleh digunakan untuk transisi yang berisi informasi.
Kartu Akhir/Kredit
Kredit atau saat kartu akhir YouTube muncul. Tidak untuk kesimpulan dengan informasi.
- Saat ada pengingat singkat untuk meminta suka, berlangganan atau mengikuti mereka di tengah konten. Jika panjang atau tentang sesuatu yang spesifik, sebaiknya pakai kategori promosi diri sendiri.
+ Saat ada pengingat singkat untuk meminta suka, berlangganan atau mengikuti di tengah konten. Jika panjang atau tentang sesuatu yang spesifik, sebaiknya pakai kategori promosi diri sendiri.
Piped
Putar di latar belakang
Lisensi
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index efb702cc2..4ceec2f66 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -209,7 +209,7 @@
Account
Guarda la cronologia
Ricorda posizione
- Ricorda la posizione a che minuto stavi guardando e cercala automaticamente.
+ Continua dall\'ultima posizione di riproduzione
Istanza di autenticazione
Usa un\'istanza diversa per le chiamate di autenticazione.
Scegli un\'istanza di autenticazione
@@ -229,4 +229,8 @@
Reddit
Connettiti a Internet attivando Wi-Fi o dati mobili.
Apri …
+ Capitoli
+ Velocità di riproduzione
+ È necessario riavviare l\'app
+ Riavviare l\'applicazione\? Le modifiche saranno applicate al successivo avvio dell\'applicazione.
\ 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 7e2ba3ec7..d0214a0ef 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -10,8 +10,8 @@
ביטול
נכנסת.
יצאת.
- בחירת עותק
- עותק אחר
+ בחירה…
+ אחר
כניסה/הרשמה
עליך להירשם לכמה ערוצים תחילה.
מ־YouTube או NewPipe
@@ -65,7 +65,7 @@
תצורת סרטונים
הורדה אל
איפה יאוחסנו פריטי המדיה שבחרת להוריד.
- כדאי לבקר באתר למידע נוסף על היישום והיכולות שלו.
+ כדאי לבקר באתר למידע נוסף על היישומון והיכולות שלו.
תרומה
GPLv3+ הוא רישיון חופשי ומתירני. מותר להשתמש, לחקור, לערוך ולשתף, עם כולם.
לחיצה כאן תבדוק אם היישומון עדכני.
@@ -105,7 +105,7 @@
מקום
כבר נכנסת. אפשר גם לצאת מהחשבון שלך.
שגיאת רשת.
- רזולוציית ברירת המחדל לסרטונים
+ רזולוציית סרטונים
מחיקת רשימת נגינה
למחוק את רשימת הנגינה\?
נוצרה רשימת נגינה.
@@ -132,9 +132,9 @@
זאת הגרסה העדכנית ביותר.
אפשר להעניק בהתאם לערך שהוא מעניק לך. צוות LibreTube קטן מהתרומה או הסיוע שלך.
איתור גרסה חדשה
- הורדות, היסטוריה
+ הורדות ואיפוס
היישומון עדכני.
- מהירות נגינה כברירת מחדל
+ מהירות נגינה
מתקדם
התאמת היישומון לטעמך.
פרק זמן ללא תוכן ממשי. יכולה להיות השהייה, תמונה ללא תזוזה, הנפשה מחזורית. אסור להשתמש בזה למעברונים שמכילים מידע.
@@ -160,8 +160,8 @@
להבה מעופפת
ציפור מוגברת
לפיד בוער
- Piped, כניסה, מינויים
- הוספת עותק משלך (על אחריותך)
+ Piped, כניסה ומינויים
+ הוספה…
שם העותק
יש למלא את השם ואת כתובת ה־API.
נא למלא כתובת שעובדת
@@ -169,10 +169,10 @@
היכרות עם הצוות של LibreTube ואיך הכול התחיל.
כתובת ל־API של העותק
הוספת עותק
- מחיקת עותקים אחרים
+ לפנות את אלו שנוספו
צורה טיפשית
תוכן קשור
- הצגת תזרימים שקשורים לצד הסרטון שמתנגן.
+ הצגת תזרימים דומים לצד הסרטון שמתנגן.
הצגת פרקים
הסתרת פרקים
בדיחות/משיקים למילוי
@@ -188,7 +188,7 @@
ללא שמע
שמע
וידאו
- מתבצעת הורדה
+ מתבצעת הורדה…
נגינה אוטומטית
הסתרת עמוד המובילים
כתובת למנשק המשתמש של העותק
@@ -196,20 +196,20 @@
איכות
התנהגות
הגדלת קפיצה
- Piped הוא חלופה בקוד פתוח למנשק המשתמש של YouTube ומספק את ה־API בו אנו משתמשים. בלי Piped, LibreTube לא היה קיים. אנו מודים למפתחים מקרב לב!
+ Piped הוא חלופה חופשית למנשק המשתמש של YouTube ומספק את ה־API בו אנו משתמשים. בלעדיו, LibreTube לא היה קיים. אנו מודים למפתחים מקרב לב!
השהיה אוטומטית
- השהיית הנגן עם כיבוי המסך.
+ השהיית הניגון עם כיבוי המסך.
שכפול רשימת הנגינה
- לנגן את הסרטון הבא אוטומטית כשהנוכחי מסתיים.
+ לנגן את הסרטון הבא אוטומטית אחרי הנוכחי.
שחזור ברירות מחדל
- להמשיך\? פעולה זו תוציא אותך מהמערכת ותאפס את כל ההגדרות שלך!
+ לאפס את כל ההגדרות ולצאת\?
חשבון
שחזור
מחיקת חשבון
מחיקת חשבון ה־Piped
היסטוריית צפייה
שמירת המיקום
- לזכור את מיקום הצפייה ולקפוץ אליו אוטומטית.
+ להמשיך מנקודת הנגינה האחרונה
עותק אימות
להשתמש בעותק אחר לשיחות מאומתות.
בחירת עותק אימות
@@ -225,10 +225,81 @@
Reddit
הטיה אוטומטית
לאורך
- יחס תצוגת וידאו
+ יחס תצוגה לסרטונים
טוויטר
- נא להתחבר לאינטרנט על ידי הפעלת הרשת האלחוטית או הנתונים הסלולריים.
+ נא להפעיל את הרשת האלחוטית או החיבור הסלולרי כדי להתחבר לאינטרנט.
פתיחה…
פרקים
מהירות נגינה
+ נדרשת הפעלה מחדש של היישומון
+ להפעיל את היישומון מחדש\?
+ תמיד
+ נבחר
+ לעולם לא
+ מסך מלא אוטומטית
+ חשיפת תוויות סרגל ניווט
+ לנגן במסך מלא כאשר המכשיר נוטה.
+ ערכת עיצוב טהורה
+ ערכת עיצוב טהורה לבנה/שחורה
+ לא נמצא נגן חיצוני. נא לוודא שמותקן אצלך אחד כזה.
+ מצב חיסכון בתעבורה
+ שמירת החיפושים
+ לעקוב אחר סרטונים בהם צפית באופן מקומי
+ לעקוב ולחפש בהיסטוריה
+ מיקומי נגינה שנשמרו
+ איפוס
+ סגנון כתוביות מערכת
+ כתוביות
+ ללא
+ תצוגה מקדימה של הסרטון
+ להציג תמונה מהסרטון בעת גרירת מחוון הנגינה.
+ להתקין את הגרסה החדשה של LibreTube כעת\?
+ לדלג על תמונות ממוזערות ותמונות אחרות.
+ כללי
+ שפה ואזור
+ מתגנן ברקע…
+ APK מתקבל…
+ כתוביות
+ תצורת שמע לנגן
+ איכות שמע
+ מיטבית
+ הנחותה ביותר
+ שפת הכתוביות
+ להמשיך\? אי אפשר לבטל את זה!
+ שם הערוץ (א-ת)
+ שם הערוץ (ת-א)
+ מיון
+ התראות על תזרימים חדשים
+ התראות על תוכן חדש מיוצרים שבחרת לעקוב אחריהם.
+ %1$s תזרימים חדשים זמינים
+ התראות
+ לבדוק כל…
+ תזרימים חדשים מאת %1$s…
+ אין עדיין היסטוריה.
+ החדש ביותר
+ הישן ביותר
+ הכי הרבה צפיות
+ הכי פחות צפיות
+ חיבור נדרש
+ הכול
+ מוגבלת
+ ברשת אלחוטית בלבד
+ תרגום
+ אפשר לסייע בתרגום היישומון
+ אין תוצאות.
+ הועתק
+ שגיאה
+ שיתוף עם זמן התחלה
+ ההורדה הצליחה
+ ייצוא מינויים
+ כפתורי דילוג
+ להציג כפתורים לדילוג לסרטון הקודם או הבא.
+ מצב רקע
+ גודל ההיסטוריה המרבי
+ לא מוגבל
+ הוספה לתור
+ כבר השקעת ביישומון %1$s דקות, הגיע הזמן לצאת להפסקה.
+ שונות
+ תזכורת הפסקה
+ הגיע הזמן להפסקה
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 8360403f3..a1cf226df 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -24,21 +24,21 @@
OK
履歴を削除
YT Musicの曲
- https://sponsor.ajay.app/ からのAPIを使用
- 有効
+ https://sponsor.ajay.app/ のAPIを使用
+ オン
無償/自己プロモーション
- インタラクションリマインダ (チャンネル登録)
+ インタラクションリマインダ (いいねとチャンネル登録)
インターミッション/イントロアニメーション
- エンドカード/クレジット
+ エンドカードとクレジット
青
紫
- 有機EL向けテーマ
+ 黒
ホーム
共有
動画
ログインしてから再試行してください。
- インスタンスを追加
- 全てのダウンロードが終了するまでお待ちください…
+ カスタム
+ 他のダウンロードが進行中です。完了するまでお待ちください。
エラーが発生しました。
登録チャンネル
地域
@@ -51,7 +51,7 @@
キャンセル
ログインしました。
ダウンロード
- インスタンスを選択
+ 選択…
ダウンロードに失敗しました。
ネットワークにエラーが発生しました。
プレイリストを作成
@@ -60,26 +60,26 @@
すでにログイン済みです。ログアウトしてください。
ログイン/新規登録
この動画をダウンロードできませんでした。
- VLCで開けませんでした。インストールされていますか?
+ VLCで開くことができません。インストールされていない可能性があります。
テーマ
サーバーに問題があります。ほかのインスタンスを試しますか?
ユーザー名とパスワードを入力する必要があります。
登録チャンネルをインポート
- 動画のデフォルト画質
+ 動画の画質
グリッドのカラム数
プレイリストを削除
このプレイリストを削除しますか?
アプリについて
システム
- ライトテーマ
- あなたのGmailアカウントではありません。
+ ライト
+ Pipedアカウント用です。
何もありません。
プレイリストに追加
言語
ダークテーマ
登録者 %1$s人
設定
- インターネット接続なし
+ インターネットに接続してください。
YT Musicの動画
位置
再試行
@@ -98,16 +98,171 @@
SponsorBlock
デフォルトのタブ
スキップしたセグメント
- 有料のプロモーション、有料の紹介、ダイレクト広告。自己宣伝や、気に入った活動/クリエイター/ウェブサイト/製品への無償の賞賛は含みません。
+ 有料のプロモーション、有料の紹介、ダイレクト広告。自己プロモーションや、クリエイター、Webサイト、製品への純粋な無償の賞賛は含みません。
無報酬や自己プロモーションであること以外は「スポンサー」に似ています。商品、寄付、協力者の情報などを含みます。
コンテンツの途中に、高評価やチャンネル登録、フォローを促す短いリマインダーがある場合。長文や具体的な内容の場合は、自己プロモーションの下に記載されます。
緑
実際のコンテンツがない部分。一時停止、静止フレーム、アニメーションの繰り返しが挙げられます。これは、情報を含むトランジションには使用されません。
- クレジットや、YouTubeのエンドカードが表示される部分。情報を含む結論には使用されません。
+ エンディング後の情報。情報を含む結論には使用されません。
ライセンス
アクセントカラー
赤
黄
- Material You
+ Material 3
通知
+ 再起動が必要です
+ 新しいバージョンを探す
+ クリックして、アプリが最新かどうかを確認します。
+ WiFiまたはモバイルデータ通信ををオンにして、インターネットに接続してください。
+ 開く…
+ デフォルトの設定に戻す
+ 本当にリセットしますか?ログアウトされ、すべての設定がリセットされます。
+ デフォルトの字幕言語
+ ファイル名
+ 動作
+ バージョン%1$sが利用可能
+ チャプター
+ アプリの開発と改良に関わるすべての人と知り合いになる。
+ GPLv3+はコピーレフトの自由なライセンスです。すべての人が、使用、研究、変更、そして共有できます。
+ 小鳥
+ インスタンス名
+ 画面が消灯したら一時停止します。
+ 再生している動画が終了したら自動で次の動画を再生します。
+ アカウント
+ Piped
+ YouTube
+ アカウント削除
+ あなたのPipedアカウントを削除します
+ 復元
+ 再生速度
+ アプリを再起動するまでこれらの変更は適用されない場合があります。今すぐ再起動しますか?
+ コメントへの返信はありません。
+ ダウンロード先
+ 内部ストレージ
+ SDカード
+ 音楽フォルダ
+ 動画フォルダ
+ バックグラウンドで再生
+ デフォルト
+ %1$s 回再生
+ アイコン
+ オフ
+ ダウンロードされたメディアを保存する場所。
+ 可能なら、あなたにとって価値のあるものを贈ってください。LibreTubeチームは、あなたの寄付や援助よりも小さな存在です。
+ Piped、ログイン、チャンネル登録
+ 追加…
+ 有効なURLを入力してください
+ バージョン %1$s
+ LibreTubeチームと出来事について知る。
+ 関連動画
+ 視聴している動画に関連するコンテンツを表示します。
+ チャプターを表示
+ チャプターを非表示
+ 穴埋めの接面/ジョーク
+ 音楽: 非音楽部分
+ プレビュー/要約
+ オン
+ GitHubにアクセスしてダウンロードしますか?
+ 外観
+ 最新バージョンを使用しています。
+ あなたは最新のバージョンを使用しています。
+ ライブ配信
+ 作者
+ ダウンロードしたメディアが保存されるフォルダの名前。
+ ダウンロードフォルダ
+ URLを共有
+ 失われた遺産
+ グラデーション
+ 炎
+ 懐中電灯
+ シェイプ
+ 光彩
+ インスタンスAPIのURL
+ インスタンスを追加
+ インスタンス名とAPIアドレスを入力してください。
+ カスタムインスタンスを削除
+ 視聴履歴
+ 位置を記憶
+ 認証インスタンス
+ 認証に別のインスタンスを使用します。
+ 認証インスタンスを選択
+ 自動
+ GitHub
+ 動画をバッファする最大秒数。
+ 動画の主な内容を理解するのに必要ない穴埋めや、ユーモアのためだけに追加された余分なシーン。
+ ミュージックビデオにのみ使用。公式ミックスに含まれない部分をカバーします。これによって、Spotifyやその他のミックスバージョンに可能な限り近い映像になるか、話し声などの邪魔なものを減少させます。
+ 動画またはそのシリーズの次の動画の詳細を示すセグメントで、追加情報が提供されない場合。ここにしか表示されないクリップが含まれている場合、間違ったカテゴリである可能性が非常に高いです。
+ プリロード
+ プレーヤーの動画形式
+ 音声と映像
+ 全画面時の向き
+ 動画のアスペクト比
+ 自動回転
+ 横
+ 縦
+ コミュニティ
+ Discord
+ Matrix
+ Telegram
+ Reddit
+ Twitter
+ ダウンロード
+ 動画の形式
+ 音声と動画の両方をダウンロードした場合のファイル形式の変換。
+ Webサイトにアクセスして、アプリの詳細や機能をもっと見る。
+ 貢献する
+ アイディアの提供、翻訳、デザイン変更、コードの記述や整理など。すればするほど便利になります。
+ 寄付
+ 再生速度
+ 詳細
+ プレイヤー
+ あなたに合わせてアプリをカスタマイズしてください。
+ ダウンロードとリセット
+ 音声なし
+ 映像なし
+ 音声
+ 映像
+ 自動再生
+ 通信料を節約するためにサムネイルとその他の画像を読み込みません。
+ 最後に再生していた位置に戻します
+ ナビゲーションバーのラベルの表示
+ 常に表示
+ 選択中のみ
+ 表示しない
+ 自動全画面
+ 端末を回転させると自動でプレイヤーを全画面にします。
+ ピュアテーマ
+ ピュアホワイト/ピュアブラック
+ 外部プレイヤーが見つかりません。インストールしてください。
+ データ節約モード
+ 検索クエリを端末に保存
+ 再生した動画を端末に保持
+ 再生と検索の履歴
+ 視聴位置
+ 再生位置をリセット
+ システムの字幕スタイル
+ 字幕
+ なし
+ 今すぐアプリをアップデートしますか?
+ 一般
+ 言語と地域
+ シークバーのプレビュー
+ シークバーを移動すると、その位置の動画をプレビューします。
+ トレンドを非表示
+ インスタンスのフロントエンドのURL
+ 解像度
+ 動作
+ 解像度とプレイヤーの動作
+ 進む
+ Pipedは、YouTubeに代わるオープンソースのWebフロントエンドで、私たちが使用するAPIを提供しています。Pipedがなければ、LibreTubeは存在しなかったでしょう。開発者たちに大感謝です!
+ 自動一時停止
+ バックグラウンドで再生中…
+ プレイリストを複製
+ ダウンロード中…
+ 字幕設定
+ APKをダウンロード中…
+ プレイヤーの音声形式
+ 音声の品質
+ 最高
+ 最低
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 8af2fab3a..a281b1cd9 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -142,7 +142,7 @@
기본 재생 속도
고급
오디오 및 비디오
- 다운로드, 기록
+ 다운로드, 재설정
실시간
이름
SD 카드
@@ -206,7 +206,7 @@
복원
시청 기록
위치 기억
- 시청 위치를 기억하고 자동으로 찾습니다.
+ 마지막 시청 위치 복원
인증 인스턴스
인증된 호출에 다른 인스턴스를 사용합니다.
자동
@@ -215,4 +215,23 @@
자동 회전
인증 인스턴스 선택
전체 화면 방향
+ WiFi 또는 모바일 데이터를 켜서 인터넷에 연결하십시오.
+ 재생 속도
+ 재시작 필요
+ 이 변경은 앱을 다시 시작해야 합니다. 지금 앱을 다시 시작하시겠습니까\? 그렇지 않으면 다음에 앱을 다시 시작할 때 변경 사항이 적용됩니다.
+ 지금 앱을 업데이트하시겠습니까\?
+ 기기가 켜지면 자동으로 플레이어 전체 화면으로 전환합니다.
+ 자동 전체화면
+ 데이터 세이버 모드
+ 검색 쿼리를 로컬에 저장
+ 시청 및 검색 기록
+ 순수한 흰색/검은색 테마
+ 외부 플레이어를 찾을 수 없습니다. 하나가 설치되어 있는지 확인하십시오.
+ 데이터를 저장하기 위해 썸네일 및 기타 이미지를 로드하지 마십시오.
+ 시청한 비디오를 로컬에서 추적
+ 순수한 테마
+ 시청 위치
+ 시청 위치 재설정
+ 시스템 캡션 스타일
+ 캡션
\ No newline at end of file
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index cc2f25f17..8e56c30a6 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -11,10 +11,10 @@
Nevar atvērt ar VLC. Tas, iespējams, nav uzinstalēts.
Tīkla kļūda.
Lejupielāde neizdevās.
- Noklusējuma video izšķirtspēja
+ Video izšķirtspēja
Atskaņošanas saraksts izveidots.
Jūs jau esat pierakstījies. Jūs varat izrakstīties no sava konta.
- Šis ir Piped kontam.
+ Pierakstīties vai reģistrēties Piped kontam.
Jūsu konts ir reģistrēts. Tagad varat abonēt kanālus.
Sākums
Pierakstīties/reģistrēties
@@ -27,20 +27,20 @@
Saglabāt
Abonementi
Bibliotēka
- Anulēt abonementu
+ Abonēts
Lietotājvārds
Pierakstīties
Atcelt
Reģions
- Izvēlēties instanci
- Pielāgota instance
+ Izvēlēties…
+ Cita
Jūs esat pierakstījies.
Pierakstieties un mēģiniet vēlreiz.
Vispirms pierakstieties vai reģistrējieties iestatījumos.
Lejupielāde pabeigta.
Atvērt ar VLC
Importēt abonementus
- Tēma
+ Motīvs
Kaut kas nogāja greizi.
Izdzēst atskaņošanas sarakstu
Vai izdzēst atskaņošanas sarakstu\?
@@ -55,7 +55,7 @@
Valoda
Sistēmas
Sistēmas
- %1$s abonementi
+ %1$s abonenti
Kvalitāte
Vispirms abonējiet dažus kanālus.
Izdarīts.
@@ -85,14 +85,14 @@
Noklusējuma cilne
Segmenti
Sponsors
- Darbības atgādinājums (spiest patīk un abonēt)
- Kad ir īss atgādinājums nospiest \"Patīk\", abonēt vai sekot viņiem satura vidū. Ja tas ir garš vai par kaut ko specifisku, tas gan nav atgādinājums, tā ir pašreklāma.
+ Darbības atgādinājums (spiest \"Patīk\" un abonēt)
+ Šeit ietilpst īsi atgādinājumi spiest \"Patīk\", abonēt vai sekot kādam satura vidū. Gari un specifiski atgādinājumi neskaitās, tie skaitās kā pašreklamēšanās.
Pārtraukums/intro animācija
- Informācija pēc video beigām. Nav domāts secinājumiem ar informāciju.
+ Šeit ietilpst viss pēc video beigām. Secinājumi ar informāciju neskaitās.
Izlaida segmentu
Ieslēgts
- Apmaksāta reklāma, apmaksāta pieminēšana un tiešas reklāmas. Nav pašreklāmai vai neapmaksātai reklāmai veidotājiem/mājaslapām/produktiem, kas viņiem patīk.
- Līdzīgs \"sponsoram\", taču neapmaksātām reklāmām vai pašreklamēšanai. Tas ietver sadaļas par precēm, ziedošanu vai informāciju par to, ar kuru viņi sadarbojās.
+ Šeit ietilpst apmaksātas un tiešas reklāmas. Pašreklāmas un neapmaksātas ziņas neskaitās.
+ Šeit ietilpst neapmaksātas ziņas un pašreklāmas, tai skaitā dažādi ziņojumi par paša precēm vai ziedojumiem, kā arī informācija par to, ar ko viņi sadarbojās.
Licence
Akcents
Sarkans
@@ -103,7 +103,7 @@
Melns
Material 3
Beigu ekrāns un titri
- Brīdis bez īsta satura. Varētu būt pauze, statisks kadrs, atkārtota animācija. Šo nevajadzētu izmantot brīžiem, kas satur informāciju.
+ Šeit ietilpst brīži bez reāla satura, piemēram, pārtraukumi, statisks kadri, atkārtotas animācijas. Brīži, kuros ietilpst kaut kāda informācija, neskaitās.
Iestatījumi
Vieta
Instance
@@ -156,56 +156,145 @@
Šī ir jaunākā versija.
Šī ir jaunākā versija.
Tehniski
- Audio un video
+ Atskaņotājs
Pielāgojiet aplikāciju tā, kā jūs to vēlaties.
- Lejupielādes, vēsture
- Noklusējuma atskaņošanas ātrums
+ Lejupielādes un atiestatīšana
+ Atskaņošanas ātrums
Tiešraide
- Pievienot pielāgotu instanci (uz jūsu atbildību)
+ Pievienot…
Ierakstiet nosaukumu un API URL.
- Notīrīt pielāgotās instances
+ Noņemt pievienotās
Lūdzu ievadiet derīgu URL
Rādīt nodaļas
Paslēpt nodaļas
- Laika sprīžiem, kas nav par tēmu vai jokiem, kas nav nepieciešami, lai saprastu video galveno saturu.
- Ne par tēmu/joki
- Mūzika: sprīži bez mūzikas
+ Šeit ietilpst brīži, kuros veidotājs novēršas no video tēmas vai kuros ir iekļauti joki, kas nav svarīgi, lai saprastu video galveno saturu.
+ Novēršanās no tēmas/joki
+ Mūzika: brīži bez mūzikas
Priekšskatījums/atkārtojums
- Sprīžiem, kas parāda, kas notiks tālāk šajā vai citā video nākotnē, bet nesniedz nekādu papildus informāciju. Ja tiek ietverti klipi, kas ir redzami tikai šeit, šī, visticamāk, nav pareizā kategorija.
- Piped, pierakstīšanās, abonementi
+ Šeit ietilpst brīži, kuros tiek iekļauta informācija par saturu, kas sagaidāms nākotnē, bet nedod papildus informāciju. Brīži, kuros tiek iekļauti klipi, kas nevienā citā mirklī neparādās, visticamāk, neietilpst šajā kategorijā.
+ Piped, pierakstīšanās un abonementi
Instances nosaukums
Instances API URL
Pievienot instanci
- Versija %1$s
- Iepazīstieties ar LibreTube komandu un kā šeit viss notiek.
+ V %1$s
+ Iepazīstieties ar visiem, kas ir iesaistīti aplikācijas veidošanā un uzlabošanā.
Saistīts saturs
- Rādīt saistītās tiešraides pie tā, ko jūs skatāties.
- Iepriekšēja ielādēšana
- Maksimālais ilgums sekundēs, cik video tiek iepriekš ielādēts.
+ Rādīt līdzīgu saturu blakus tam, ko jūs skatāties.
+ Iepriekš ielādēt
+ Maksimālais ilgums sekundēs, cik tālu video tiek iepriekš ielādēts.
Atskaņotāja video formāts
- Tikai priekš mūzikas video. Tas ir laika sprīžiem, kas nav oficiālajā dziesmas versijā. Beigās video vajadzētu būt tik līdzīgam Spotify vai jebkurai citai dziesmas versijai, cik vien iespējams vai ar samazinātu runāšanas daudzumu un uzmanības novēršanu.
+ Tikai priekš mūzikas video. Šeit ietilpst brīži, kas nav daļas no oficiālās dziesmas. Beigu beigās video vajadzētu līdzināties oficiālajai dziesmas versijai (piemēram, Spotify versijai), cik vien tuvu iespējams.
Automātiski atskaņot
Atjaunot noklusējumu
- Vai esat pārliecināts\? Šī darbība jūs izrakstīs un iestatīs visus iestatījumus uz noklusējumu!
+ Atiestatīt visus iestatījumus un izrakstīties\?
Nav audio
Nav video
Audio
Video
- Lejupielādē
+ Lejupielādē…
Paslēpt tendenču lapu
URL instances frontend
Kvalitāte
Uzvedība
Kvalitāte un atskaņotāja uzvedība
- Meklēšanas iedaļa
- Piped ir atvērta pirmkoda alternatīva mājaslapa priekš YouTube, kas sniedz API, ko mēs izmantojam. Bez Piped, LibreTube neeksistētu. Milzīgs paldies viņu veidotājiem!
- Automātiska pauzēšana
- Iepauzēt atskaņotāju, kamēr ekrāns ir izslēgts.
- Automātiski atskaņot nākamo video, kad pašreizējais ir beidzies.
+ Patīšanas iedaļa
+ Piped ir alternatīva priekš YouTube ar atvērtu pirmkodu, kas sniedz API, ko mēs izmantojam. LibreTube bez tā nepastāvētu. Milzīgs paldies viņu veidotājiem!
+ Automātiski iepauzēt
+ Pārtraukt atskaņošanu, kad ekrāns tiek izslēgts.
+ Automātiski atskaņot nākamo video, kad iepriekšējais beidzas.
Duplicēt atskaņošanas sarakstu
Izdzēst kontu
Izdzēst Piped kontu
Atjaunot
Konts
Skatīšanās vēsture
+ Lūdzu, ieslēdziet Wi-Fi vai mobilos datus, lai savienotos ar internetu.
+ Atvērt…
+ Nodaļas
+ Nepieciešams restartēt aplikāciju
+ Šīs izmaiņas, iespējams, netiks veiktas bez aplikācijas restartēšanas. Vai vēlaties restartēt aplikāciju tagad\?
+ Autentificētā instance
+ Atskaņošanas ātrums
+ Atcerēties skatīšanās vietu
+ Atcerēties vietu, kurā pārtraucāt skatīties video, un automātiski uz to aiztīt.
+ Lietojiet citu instanci priekš autentificētiem pieprasījumiem.
+ Izvēlēties autentificēto instanci
+ Automātiska
+ GitHub
+ Noteikt no video malu attiecības
+ Noteikt no ierīces orientācijas
+ Audio un video
+ Pilnekrāna orientācija
+ Horizontāla
+ Vertikāla
+ Kopiena
+ Discord
+ Matrix
+ Telegram
+ Reddit
+ Twitter
+ Rādīt tekstu navigācijas panelī
+ Vienmēr
+ Nekad
+ Automātiski pāriet uz pilnekrāna režīmu, kad ierīce tiek pagriezta
+ Tīri melnbalts motīvs
+ Ārējais atskaņotājs netika atrasts. Lūdzu, pārliecinieties, ka jums tas ir ieinstalēts.
+ Neielādēt sīktēlus un citus attēlus, lai ietaupītu datus
+ Uzglabāt skatīšanās vēsturi ierīcē
+ Atcerēties meklēšanas vēsturi
+ Skatīšanās un meklēšanas vēsture
+ Atiestatīt
+ Atcerētās atskaņošanas vietas
+ Subtitri
+ Nav
+ Vispārēji
+ Valoda un reģions
+ Video priekšskatījums
+ Priekšskatīt video kamēr tas tiek patīts, izmantojot meklēšanas joslu.
+ Datu taupīšanas režīms
+ Izvēlēts
+ Automātiski pāriet pilnekrāna režīmā
+ Tīrais motīvs
+ Sistēmas subtitru dizains
+ Vai vēlaties ielādēt jauno LibreTube versiju\?
+ Atskaņo fonā…
+ Subtitri
+ Lejupielādē APK…
+ Audio kvalitāte
+ Sliktākā
+ Atskaņotāja audio formāts
+ Augstākā
+ Subtitru valoda
+ Notifikācijas
+ Notifikācijas jaunam saturam
+ Sūtīt notifikācijas par jaunu saturu no veidotājiem, kuriem jūs sekojat.
+ Pieejami %1$s jauni vienumi
+ Jauns saturs no %1$s…
+ Vai esat pārliecināts\? Šī darbība ir neatgriezeniska.
+ Jaunākie
+ Senākie
+ Skatītākie
+ Kanāla nosaukums (A-Z)
+ Kanāla nosaukums (Z-A)
+ Filtrēšana
+ Ierobežots
+ Nav rezultātu.
+ Neskatītākie
+ Vēsture ir tukša.
+ Nepieciešamais savienojums
+ Visi
+ Tikai caur Wi-Fi
+ Tulkošana
+ Palīdziet iztulkot lietotni
+ Pārbaudīt ik pēc…
+ Kļūda
+ Nokopēts
+ Lejupielāde veiksmīga
+ Eksportēt abonementus
+ Kopīgot ar sākšanas laiku
+ Izlaišanas pogas
+ Rādīt pogas, kas ļauj doties uz nākamo vai iepriekšējo video
+ Maksimālais vēstures izmērs
+ Fona režīms
+ Neierobežots
\ No newline at end of file
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index e44f52fa5..cf9bda92f 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -7,8 +7,8 @@
Logg inn
Registrering
Registrert. Du kan nå abonnere på de kanalene du ønsker.
- Velg en instans
- Egendefinert instans
+ Velg …
+ Egendefinert
Region
Logg inn/registrer deg
Nedlasting fullført.
@@ -18,9 +18,9 @@
Nedlasting mislyktes.
Kan ikke åpne i VLC. Kan hende det ikke er installert.
Problem med tjeneren. Prøv en annen instans\?
- Forvalgt video-oppløsning
+ Video-oppløsning
Slett spilleliste
- Dette er for en LibreTube-konto.
+ Dette er for en Piped-konto.
Ferdig.
Mislykket :(
Om
@@ -102,7 +102,7 @@
Videoformat
Direkte
Piped
- Bli kjent med LibreTube-laget og hvordan alt går for seg.
+ Bli kjent med alle.
Søkehistorikk
YT Musikk-spor
YT Musikk-videoer
@@ -124,10 +124,10 @@
Der nedlastet media lagres.
Send inn idéer, oversettelser, designendringer, rens og skriv kode. Desto mer som blir gjort, desto bedre blir det.
GPLv3+ er en gemenfrihetslig lisens. Bruk, studer, endre, og del; med alle.
- Klikk for å finne ut om programmet er av nyeste dato.
+ Klikk for å sjekke at du har siste versjon.
Kjører siste versjon.
Tilpass programmet til din smak.
- Nedlastninger, historikk
+ Nedlastninger, og tilbakestilling
Fasjonabel flamme
Finskodd fakkel
Formodentlig formet
@@ -158,18 +158,18 @@
Bidra
Doner
Du kjører den siste versjonen.
- Forvalgt avspillingshastighet
+ Avspillingshastighet
Avansert
- Lyd og video
- Piped, innlogging, abonnementer
- Legg til en egendefinert instans (på egen risiko)
+ Avspiller
+ Piped, innlogging, og abonnementer
+ Legg til …
Navn på instans
Nettadresse for instans-API
Legg til instans
Skriv inn en nettadresse som virker
Bli kjent med LibreTube-laget og alt som skjer.
Fyll inn navnet og API-nettadressen.
- Versjon %1$s
+ V %1$s
Tøm egendefinerte instanser
Relatert innhold
Vis videoer relatert til det du ser.
@@ -182,7 +182,7 @@
For avsporinger som kun er lagt til for å fylle tid, eller være morsomme, og ikke er nødvendige for å forstå videoens hovedinnhold.
Kun til bruk for musikkvideoer. Det bør dekke de delene av videoen som ikke er i den offisielle miksen. Til slutt skal videoen utgjøre det som finnes på Spotify eller annen mikset versjon så nært som mulig, eller redusere snakking og andre distraksjoner.
Maks. antall sekunders mellomlagring av videoer.
- For segmenter som viser hva som følger i denne eller kommende videoer i serien, men ikke har ytterligere info. Hvis dette inkluderer klipp som kun vises her er det antagelig ikke riktig kategori.
+ For segmenter som viser kommende innhold i serien, men ikke gir ytterligere info. Hvis dette inkluderer klipp som kun vises her er det antagelig ikke riktig kategori.
Forhåndsinnlasting
Laster ned …
Spill automatisk
@@ -194,14 +194,14 @@
Ingen video
Kvalitet
Oppførsel
- Kvalitet og avspilleroppførsel
+ Kvalitet, og avspilleroppførsel
Blafringsmengde
- Piped er en fri alternativ grenseflate for YouTube som tilbyr API-et programmet bruker. Uten Piped ville ikke LibreTube eksistert. Stor takk til utviklerne deres.
- Logg ut og tilbakestill alle innstillinger\?
+ Piped er en fri alternativ grenseflate for YouTube som tilbyr API-et programmet bruker. Uten det ville ikke LibreTube eksistert. Stor takk til utviklerne deres.
+ Tilbakestill alle innstillinger og logg ut\?
Spill neste video når nåværende er fullført.
Gjenopprett forvalg
Automatisk pause
- Sett avspilleren på pause når skjermen skrus av.
+ Sett avspilling når skjermen skrus av.
Dupliser spillelisten
Slett konto
Slett din Piped-konto
@@ -214,5 +214,82 @@
GitHub
Instans for identitetsbekreftelse
Bruk en annen instans for identitetsbekreftede kall.
- Husk visningsposisjon og begynn derfra.
+ Fortsett fra tidligere visningsposisjon
+ Kapitler
+ Programomstart kreves
+ Skru på Wi-Fi eller mobildata for å koble deg til Internett.
+ Start programmet på ny\?
+ Åpne …
+ Avspillingshastighet
+ Lyd og bilde
+ Fullskjerm
+ Video-sideretning
+ Automatisk
+ Liggende
+ Stående
+ Gemenskap
+ Discord
+ Matrix
+ Telegram
+ Reddit
+ Twitter
+ Ingen
+ Alltid
+ Ren svarthvitt-drakt
+ Aldri
+ Ren drakt
+ Generelt
+ Automatisk fullskjerm
+ Datasparingsmodus
+ Språk, og region
+ Husk søk
+ Merknader
+ %1$s nye strømmer tilgjengelig
+ Nye strømmer av %1$s …
+ Nyligst
+ Eldst
+ Flest visninger
+ Færrest visninger
+ Kanalnavn (A-Å)
+ Kanalnavn (Å-A)
+ Oversettelse
+ Kvotebasert
+ Historikken er tom.
+ Kun Wi-Fi
+ Påkrevd nettverkstype
+ Alle
+ Laster ned APK …
+ Lydformat for avspiller
+ Lydkvalitet
+ Best
+ Verst
+ Undertekstspråk
+ Er du sikker\? Dette kan ikke angres.
+ Abonnementsnyneter
+ Merknader om nytt innhold fra skapere du følger.
+ Sjekker hvert …
+ Sortering
+ Bistå oversettelsen av programmet
+ Resultatløst.
+ Kopiert
+ Feil
+ Del med starttid
+ Nedlastet
+ Installer den nye versjonen nå\?
+ Valgte
+ Fullskjerm når enheten ligger.
+ Installer et avspillerprogram først.
+ Spar data ved å unngå bilder.
+ Holde rede på sette videoer
+ Visnings- og søkehistorikk
+ Huskede visningsposisjoner
+ Tilbakestill
+ Systemundertekststil
+ Undertekster
+ Videoforhåndsvisning
+ Vis bilde av videoen der framdriftsindikatoren trekkes.
+ Spiller i bakgrunnen …
+ Undertekstinnstillinger
+ Synlighet for navigeringsfeltets etikett
+ Eksporter abonnementer
\ 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 90b219957..4d9db8538 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,6 +1,6 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 39c561d75..cd05f76ae 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,6 +1,6 @@
-
-
-
-
-
-
-