Merge branch 'libre-tube:master' into master

This commit is contained in:
XelXen 2022-08-13 11:55:54 +05:30 committed by GitHub
commit 6ccec878d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
279 changed files with 9262 additions and 4700 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,2 +1,2 @@
patreon: LibreTubeTeam
custom: https://libre-tube.github.io#donate
custom: https://github.com/libre-tube/LibreTube#-donate

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -1,21 +1,29 @@
<div align="center">
<img src="https://libre-tube.github.io/assets/gh-banner.png" width="auto" height="auto" alt="LibreTube">
<img src="https://libre-tube.github.io/images/gh-banner.png" width="auto" height="auto" alt="LibreTube">
[![GPL-v3](https://libre-tube.github.io/assets/license-widget.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Matrix](https://libre-tube.github.io/assets/mat-widget.svg)](https://matrix.to/#/#LibreTube:matrix.org)
[![Telegram](https://libre-tube.github.io/assets/tg-widget.svg)](https://t.me/libretube)
[![Twitter](https://libre-tube.github.io/assets/tw-widget.svg)](https://twitter.com/libretube)
[![Reddit](https://libre-tube.github.io/assets/rd-widget.svg)](https://www.reddit.com/r/Libretube/)
[![GPL-v3](https://libre-tube.github.io/images/license-widget.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Matrix](https://libre-tube.github.io/images/mat-widget.svg)](https://matrix.to/#/#LibreTube:matrix.org)
[![Telegram](https://libre-tube.github.io/images/tg-widget.svg)](https://t.me/libretube)
[![Twitter](https://libre-tube.github.io/images/tw-widget.svg)](https://twitter.com/libretube)
[![Reddit](https://libre-tube.github.io/images/rd-widget.svg)](https://www.reddit.com/r/Libretube/)
</div><div align="center" style="width:100%; display:flex; justify-content:space-between;">
[<img src="https://libre-tube.github.io/assets/fdrload.png" alt="Get it on F-Droid" width="30%">](https://f-droid.org/en/packages/com.github.libretube/)
[<img src="https://libre-tube.github.io/assets/izzyload.png" alt="Get it on IzzyOnDroid" width="30%">](https://apt.izzysoft.de/fdroid/index/apk/com.github.libretube)<br/>
[<img src="https://libre-tube.github.io/assets/ghload.png" alt="Get it on GitHub" width="30%">](https://github.com/libre-tube/LibreTube/releases/latest)
[<img src="https://libre-tube.github.io/assets/tgload.png" alt="Get it on GitHub" width="30%">](https://t.me/LibreTube)
[<img src="https://libre-tube.github.io/images/fdrload.png" alt="Get it on F-Droid" width="30%">](https://f-droid.org/en/packages/com.github.libretube/)
[<img src="https://libre-tube.github.io/images/izzyload.png" alt="Get it on IzzyOnDroid" width="30%">](https://apt.izzysoft.de/fdroid/index/apk/com.github.libretube)<br/>
[<img src="https://libre-tube.github.io/images/ghload.png" alt="Get it on GitHub" width="30%">](https://github.com/libre-tube/LibreTube/releases/latest)
[<img src="https://libre-tube.github.io/images/tgload.png" alt="Get it on GitHub" width="30%">](https://t.me/LibreTube)
</div>
## 📔 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
<div style="width:100%; display:flex; justify-content:space-between;">
@ -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
<a href="https://hosted.weblate.org/projects/libretube/#languages">
@ -73,3 +79,6 @@ If opening an issue without following the issue template, we will ignore the iss
<a href="https://gitlab.com/libretube/LibreTube">GitLab</a></p>
<a href="https://notabug.org/LibreTube/LibreTube">NotABug</a></p>
<div align="right">
<b><a href="https://github.com/libre-tube/LibreTube#-about">↥ Back to top</a></b>
</div>

View File

@ -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()
}

View File

@ -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

View File

@ -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"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
@ -7,7 +8,9 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
@ -20,17 +23,25 @@
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Purple">
<activity
android:name=".activities.Player"
android:configChanges="orientation|screenSize"
android:exported="false" />
android:theme="@style/Theme.Purple.Pure"
tools:targetApi="n">
<activity
android:name=".activities.NoInternetActivity"
android:label="@string/noInternet" />
<activity
android:name=".activities.SettingsActivity"
android:label="@string/settings" />
<activity
android:name=".activities.AboutActivity"
android:label="@string/settings" />
<activity
android:name=".activities.CommunityActivity"
android:label="@string/settings" />
<activity
android:name=".activities.MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
@ -281,8 +292,17 @@
<service
android:name=".services.ClosingService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false" />
android:exported="false" />
<service
android:name=".services.UpdateService"
android:enabled="true"
android:exported="false" />
<service
android:name=".services.BackgroundMode"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

@ -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"

View File

@ -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<String>()
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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
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,12 +106,14 @@ 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")) {
startFragmentId =
when (PreferenceHelper.getString(PreferenceKeys.DEFAULT_TAB, "home")) {
"home" -> R.id.homeFragment
"subscriptions" -> R.id.subscriptionsFragment
"library" -> R.id.libraryFragment
@ -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,21 +158,139 @@ class MainActivity : AppCompatActivity() {
}
binding.toolbar.title = ThemeHelper.getStyledAppName(this)
binding.toolbar.setNavigationOnClickListener {
// settings activity stuff
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
}
binding.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_search -> {
/**
* handle error logs
*/
val log = PreferenceHelper.getErrorLog()
if (log != "") ErrorDialog().show(supportFragmentManager, null)
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()
}
}
},
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)
}
}
@ -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
}
}
}
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)
} 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())
videoId = videoId
.replace("v=", "")
}
loadWatch(bundle)
loadVideo(videoId, data.query)
} else {
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<LinearLayout>(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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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,11 +30,12 @@ class SettingsActivity : AppCompatActivity() {
}
override fun onBackPressed() {
if (Globals.isCurrentViewMainSettings) {
when (supportFragmentManager.findFragmentById(R.id.settings)) {
is MainSettings -> {
super.onBackPressed()
finishAndRemoveTask()
} else {
Globals.isCurrentViewMainSettings = true
}
else -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainSettings())
@ -55,6 +43,7 @@ class SettingsActivity : AppCompatActivity() {
changeTopBarText(getString(R.string.settings))
}
}
}
fun changeTopBarText(text: String) {
if (this::binding.isInitialized) binding.topBarTextView.text = text

View File

@ -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<StreamItem>,
@ -26,47 +25,39 @@ class ChannelAdapter(
}
fun updateItems(newItems: List<StreamItem>) {
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)

View File

@ -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<ChapterSegment>,
private val exoPlayer: ExoPlayer
) : RecyclerView.Adapter<ChaptersViewHolder>() {
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
}

View File

@ -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<MotionLayout>(R.id.mainMotionLayout)
if (mainMotionLayout.progress == 0.toFloat()) {
mainMotionLayout.transitionToEnd()
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
.transitionToEnd()
}
} catch (e: Exception) {
}
NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
}
repliesRecView.layoutManager = LinearLayoutManager(root.context)
val repliesAdapter = RepliesAdapter(CommentsPage().comments)

View File

@ -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,45 +53,35 @@ 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() {
fun removeFromPlaylist(position: Int) {
videoFeed.removeAt(position)
activity.runOnUiThread { notifyDataSetChanged() }
CoroutineScope(Dispatchers.IO).launch {
val response = try {
try {
RetrofitInstance.authApi.removeFromPlaylist(
token,
PreferenceHelper.getToken(),
PlaylistId(playlistId = playlistId, index = position)
)
} catch (e: IOException) {
@ -102,22 +91,9 @@ class PlaylistAdapter(
} 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())
}
}
}
run()
}
}
class PlaylistViewHolder(val binding: PlaylistRowBinding) : RecyclerView.ViewHolder(binding.root)

View File

@ -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<Playlists>,
private val childFragmentManager: FragmentManager,
private val activity: Activity
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
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) {

View File

@ -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<Comment>
@ -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<MotionLayout>(R.id.mainMotionLayout)
if (mainMotionLayout.progress == 0.toFloat()) {
mainMotionLayout.transitionToEnd()
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
.transitionToEnd()
}
} catch (e: Exception) {
}
NavigationHelper.navigateVideo(root.context, reply.commentorUrl)
}
}
}

View File

@ -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<SearchItem>,
@ -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,106 +77,76 @@ 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) {
if (isSubscribed == null) return@launch
binding.searchSubButton.visibility = View.VISIBLE
binding.searchSubButton.setOnClickListener {
if (!isSubscribed) {
subscribe(token, channelId)
if (isSubscribed == false) {
SubscriptionHelper.subscribe(channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.unsubscribe)
isSubscribed = true
} else {
unsubscribe(token, channelId)
SubscriptionHelper.unsubscribe(channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.subscribe)
isSubscribed = false
@ -189,54 +154,24 @@ class SearchAdapter(
}
}
}
}
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
}

View File

@ -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<String>,
private val editText: EditText,
private val searchFragment: SearchFragment
private val searchView: SearchView
) :
RecyclerView.Adapter<SearchHistoryViewHolder>() {
@ -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)
}
}
}

View File

@ -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<String>,
private var editText: EditText,
private val searchFragment: SearchFragment
private val searchView: SearchView
) :
RecyclerView.Adapter<SearchSuggestionsViewHolder>() {
@ -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)
}
}
}

View File

@ -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<StreamItem>,
private val childFragmentManager: FragmentManager
) : RecyclerView.Adapter<SubscriptionViewHolder>() {
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<MotionLayout>(R.id.mainMotionLayout)
if (mainMotionLayout.progress == 0.toFloat()) {
mainMotionLayout.transitionToEnd()
activity.findViewById<MotionLayout>(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)

View File

@ -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<Subscription>) :
RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
val TAG = "SubChannelAdapter"
private var subscribed = true
private var isLoading = false
override fun getItemCount(): Int {
return subscriptions.size
}
@ -38,67 +28,28 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscrip
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
val subscription = subscriptions[position]
var subscribed = true
holder.binding.apply {
subscriptionChannelName.text = subscription.name
Picasso.get().load(subscription.avatar).into(subscriptionChannelImage)
ConnectionHelper.loadImage(subscription.avatar, subscriptionChannelImage)
root.setOnClickListener {
val activity = root.context as MainActivity
val bundle = bundleOf("channel_id" to subscription.url)
activity.navController.navigate(R.id.channelFragment, bundle)
NavigationHelper.navigateChannel(root.context, subscription.url)
}
subscriptionSubscribe.setOnClickListener {
if (!isLoading) {
isLoading = true
val channelId = subscription.url?.replace("/channel/", "")!!
val channelId = subscription.url.toID()
if (subscribed) {
unsubscribe(root.context, channelId)
subscriptionSubscribe.text = root.context.getString(R.string.subscribe)
SubscriptionHelper.unsubscribe(channelId)
subscribed = false
} else {
subscribe(root.context, channelId)
subscriptionSubscribe.text =
root.context.getString(R.string.unsubscribe)
}
}
}
}
}
private fun subscribe(context: Context, channelId: String) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
try {
val token = PreferenceHelper.getToken(context)
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
SubscriptionHelper.subscribe(channelId)
subscribed = true
isLoading = false
}
}
run()
}
private fun unsubscribe(context: Context, channelId: String) {
fun run() {
CoroutineScope(Dispatchers.IO).launch {
try {
val token = PreferenceHelper.getToken(context)
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
subscribed = false
isLoading = false
}
}
run()
}
}

View File

@ -1,96 +1,73 @@
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.extensions.setFormattedDuration
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 TrendingAdapter(
private val videoFeed: List<StreamItem>,
private val childFragmentManager: FragmentManager
) : RecyclerView.Adapter<TrendingViewHolder>() {
private val streamItems: List<StreamItem>,
private val childFragmentManager: FragmentManager,
private val showAllAtOne: Boolean = true
) : RecyclerView.Adapter<SubscriptionViewHolder>() {
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<MotionLayout>(R.id.mainMotionLayout)
if (mainMotionLayout.progress == 0.toFloat()) {
mainMotionLayout.transitionToEnd()
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
.transitionToEnd()
NavigationHelper.navigateChannel(root.context, trending.uploaderUrl)
}
} catch (e: Exception) {
}
}
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)

View File

@ -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<WatchHistoryItem>,
@ -24,10 +20,10 @@ class WatchHistoryAdapter(
RecyclerView.Adapter<WatchHistoryViewHolder>() {
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<MotionLayout>(R.id.mainMotionLayout)
if (mainMotionLayout.progress == 0.toFloat()) {
mainMotionLayout.transitionToEnd()
activity.findViewById<MotionLayout>(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)
}
}

View File

@ -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<String>().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 {
}
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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("")
}
}

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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)
)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
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)
}
}

View File

@ -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")
}

View File

@ -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 ->

View File

@ -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 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
}
}

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}
})
}

View File

@ -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
}

View File

@ -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,17 +53,14 @@ 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()
}
}
refreshChannel()
binding.channelRefresh.setOnRefreshListener {
refreshChannel()
@ -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
}
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated
runOnUiThread {
if (response.subscribed == true) {
isSubscribed = true
if (isSubscribed == true) {
binding.channelSubscribe.text = getString(R.string.unsubscribe)
}
if (response.subscribed != null) {
binding.channelSubscribe.apply {
setOnClickListener {
text = if (isSubscribed) {
unsubscribe()
binding.channelSubscribe.setOnClickListener {
binding.channelSubscribe.text = if (isSubscribed == true) {
SubscriptionHelper.unsubscribe(channelId!!)
isSubscribed = false
getString(R.string.subscribe)
} else {
subscribe()
SubscriptionHelper.subscribe(channelId!!)
isSubscribed = true
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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
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)
// waiting for the query to change
viewModel.searchQuery.observe(viewLifecycleOwner) {
showData(it)
}
}
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 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, autoTextView: EditText) {
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
}
nextPage = response.nextpage
searchAdapter?.updateItems(response.items!!)
isLoading = false
runOnUiThread {
if (viewModel.searchQuery.value != "") {
binding.suggestionsRecycler.adapter = suggestionsAdapter
}
}
}
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)
binding.suggestionsRecycler.visibility = View.GONE
binding.historyEmpty.visibility = View.VISIBLE
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<StreamItem> = listOf()
private var sortOrder = "most_recent"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -47,43 +52,43 @@ 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.subProgress.visibility = View.VISIBLE
val grid = PreferenceHelper.getString(
requireContext(),
"grid",
PreferenceKeys.GRID_COLUMNS,
resources.getInteger(R.integer.grid_items).toString()
)!!
)
binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
fetchFeed(binding.subFeed, binding.subProgress)
fetchFeed()
binding.subRefresh.setOnRefreshListener {
fetchChannels(binding.subChannels)
fetchFeed(binding.subFeed, binding.subProgress)
fetchChannels()
fetchFeed()
}
binding.sortTV.setOnClickListener {
showSortDialog()
}
binding.toggleSubs.visibility = View.VISIBLE
var loadedSubbedChannels = false
binding.toggleSubs.setOnClickListener {
binding.toggle.animate().rotationBy(180F).setDuration(100).start()
if (!binding.subChannels.isVisible) {
if (!binding.subChannelsContainer.isVisible) {
if (!loadedSubbedChannels) {
binding.subChannels.layoutManager = LinearLayoutManager(context)
fetchChannels(binding.subChannels)
fetchChannels()
loadedSubbedChannels = true
}
binding.subChannels.visibility = View.VISIBLE
binding.subFeed.visibility = View.GONE
binding.subChannelsContainer.visibility = View.VISIBLE
binding.subFeedContainer.visibility = View.GONE
} else {
binding.subChannels.visibility = View.GONE
binding.subFeed.visibility = View.VISIBLE
binding.subChannelsContainer.visibility = View.GONE
binding.subFeedContainer.visibility = View.VISIBLE
}
}
@ -100,16 +105,30 @@ class SubscriptionsFragment : Fragment() {
}
}
}
} else {
binding.subRefresh.isEnabled = 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)
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,5 @@
package com.github.libretube.interfaces
interface DoubleTapInterface {
fun onEvent(x: Float)
}

View File

@ -0,0 +1,16 @@
package com.github.libretube.interfaces
interface PlayerOptionsInterface {
fun onAutoplayClicked()
fun onCaptionClicked()
fun onQualityClicked()
fun onPlaybackSpeedClicked()
fun onAspectRatioClicked()
fun onRepeatModeClicked()
}

View File

@ -0,0 +1,12 @@
package com.github.libretube.models
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SearchViewModel : ViewModel() {
var searchQuery = MutableLiveData<String>()
fun setQuery(query: String?) {
this.searchQuery.value = query
}
}

View File

@ -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
)

View File

@ -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
)

View File

@ -7,6 +7,4 @@ data class CommentsPage(
val comments: MutableList<Comment> = arrayListOf(),
val disabled: Boolean? = null,
val nextpage: String? = ""
) {
constructor() : this(arrayListOf(), null, "")
}
)

View File

@ -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
)

View File

@ -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<NewPipeSubscription>? = null
)

View File

@ -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
)

View File

@ -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)
}
)

View File

@ -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<Float>?
) {
constructor() : this("", "", arrayListOf())
}
val actionType: String? = null,
val category: String? = null,
val segment: List<Float>? = arrayListOf()
)

View File

@ -5,6 +5,4 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Segments(
val segments: MutableList<Segment> = arrayListOf()
) {
constructor() : this(arrayListOf())
}
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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?,

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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<Preference>("clear_history")
clearHistory?.setOnPreferenceClickListener {
PreferenceHelper.removePreference(requireContext(), "search_history")
true
}
// clear watch history and positions
val clearWatchHistory = findPreference<Preference>("clear_watch_history")
clearWatchHistory?.setOnPreferenceClickListener {
PreferenceHelper.removePreference(requireContext(), "watch_history")
PreferenceHelper.removePreference(requireContext(), "watch_positions")
true
}
val resetSettings = findPreference<Preference>("reset_settings")
val resetSettings = findPreference<Preference>(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()
}
}

View File

@ -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<ListPreference>("theme_toggle")
val themeToggle = findPreference<ListPreference>(PreferenceKeys.THEME_MODE)
themeToggle?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val pureTheme = findPreference<SwitchPreferenceCompat>("pure_theme")
val pureTheme = findPreference<SwitchPreferenceCompat>(PreferenceKeys.PURE_THEME)
pureTheme?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val accentColor = findPreference<ListPreference>("accent_color")
val accentColor = findPreference<ListPreference>(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<ListPreference>("icon_change")
val iconChange = findPreference<ListPreference>(PreferenceKeys.APP_ICON)
iconChange?.setOnPreferenceChangeListener { _, newValue ->
ThemeHelper.changeIcon(requireContext(), newValue.toString())
true
}
val gridColumns = findPreference<ListPreference>("grid")
gridColumns?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val hideTrending = findPreference<SwitchPreferenceCompat>("hide_trending_page")
hideTrending?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val labelVisibilityMode = findPreference<ListPreference>("label_visibility")
val labelVisibilityMode = findPreference<ListPreference>(PreferenceKeys.LABEL_VISIBILITY)
labelVisibilityMode?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val systemCaptionStyle =
findPreference<SwitchPreferenceCompat>(PreferenceKeys.SYSTEM_CAPTION_STYLE)
val captionSettings = findPreference<Preference>(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
}
}

View File

@ -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<ListPreference>("language")
language?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val autoRotation = findPreference<SwitchPreferenceCompat>(PreferenceKeys.AUTO_ROTATION)
autoRotation?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val hideTrending = findPreference<SwitchPreferenceCompat>(PreferenceKeys.HIDE_TRENDING_PAGE)
hideTrending?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val breakReminder = findPreference<ListPreference>(PreferenceKeys.BREAK_REMINDER)
breakReminder?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
}
}

View File

@ -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<Preference>(PreferenceKeys.CLEAR_SEARCH_HISTORY)
clearHistory?.setOnPreferenceClickListener {
showClearDialog(R.string.clear_history, "search_history")
true
}
// clear watch history and positions
val clearWatchHistory = findPreference<Preference>(PreferenceKeys.CLEAR_WATCH_HISTORY)
clearWatchHistory?.setOnPreferenceClickListener {
showClearDialog(R.string.clear_history, "watch_history")
true
}
// clear watch positions
val clearWatchPositions = findPreference<Preference>(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()
}
}

View File

@ -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<String>
private lateinit var createFile: ActivityResultLauncher<String>
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<String>()
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
getContent =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
ImportHelper(requireActivity()).importSubscriptions(uri)
}
createFile = registerForActivityResult(
ActivityResultContracts.CreateDocument()
) { uri: Uri? ->
ImportHelper(requireActivity()).exportSubscriptions(uri)
}
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()
}
}
}
super.onCreate(savedInstanceState)
}
@ -115,25 +53,24 @@ class InstanceSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.instance))
val instance = findPreference<ListPreference>("selectInstance")
val instance = findPreference<ListPreference>(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<ListPreference>("selectAuthInstance")
val authInstance = findPreference<ListPreference>(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<SwitchPreferenceCompat>("auth_instance_toggle")
val authInstanceToggle =
findPreference<SwitchPreferenceCompat>(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<Preference>("customInstance")
val customInstance = findPreference<Preference>(PreferenceKeys.CUSTOM_INSTANCE)
customInstance?.setOnPreferenceClickListener {
val newFragment = CustomInstanceDialog()
newFragment.show(childFragmentManager, "CustomInstanceDialog")
newFragment.show(childFragmentManager, CustomInstanceDialog::class.java.name)
true
}
val clearCustomInstances = findPreference<Preference>("clearCustomInstances")
val clearCustomInstances = findPreference<Preference>(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<Preference>("login_register")
val token = PreferenceHelper.getToken(requireContext())
val login = findPreference<Preference>(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<Preference>("delete_account")
val deleteAccount = findPreference<Preference>(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<Preference>("import_from_yt")
importFromYt?.setOnPreferenceClickListener {
importSubscriptions()
val importSubscriptions = findPreference<Preference>(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<Preference>(PreferenceKeys.EXPORT_SUBS)
exportSubscriptions?.setOnPreferenceClickListener {
createFile.launch("subscriptions.json")
true
}
}
private fun initCustomInstances(instancePref: ListPreference) {
val customInstances = PreferenceHelper.getCustomInstances(requireContext())
lifecycleScope.launchWhenCreated {
val customInstances = PreferenceHelper.getCustomInstances()
val instanceNames = arrayListOf<String>()
val instanceValues = arrayListOf<String>()
// fetch official public instances
val response = try {
RetrofitInstance.api.getInstances("https://instances.tokhmi.xyz/")
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
response.forEach {
if (it.name != null && it.api_url != null) {
instanceNames += it.name!!
instanceValues += it.api_url!!
}
}
var instanceNames = resources.getStringArray(R.array.instances)
var instanceValues = resources.getStringArray(R.array.instancesValue)
customInstances.forEach { instance ->
instanceNames += instance.name
instanceValues += instance.apiUrl
}
runOnUiThread {
// add custom instances to the list preference
instancePref.entries = instanceNames
instancePref.entryValues = instanceValues
instancePref.entries = instanceNames.toTypedArray()
instancePref.entryValues = instanceValues.toTypedArray()
instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
val text = preference.entry
if (TextUtils.isEmpty(text)) {
"kavin.rocks (Official)"
} else {
text
preference.entry
}
}
}
}
private fun logout() {
PreferenceHelper.setToken(requireContext(), "")
PreferenceHelper.setToken("")
Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
}
private fun fetchInstance() {
lifecycleScope.launchWhenCreated {
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<String> = ArrayList()
val listEntryValues: MutableList<String> = ArrayList()
for (item in response) {
listEntries.add(item.name!!)
listEntryValues.add(item.api_url!!)
}
// add custom instances to the list
val entries = listEntries.toTypedArray<CharSequence>()
val entryValues = listEntryValues.toTypedArray<CharSequence>()
runOnUiThread {
val instance = findPreference<ListPreference>("selectInstance")
instance?.entries = entries
instance?.entryValues = entryValues
instance?.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
val text = preference.entry
if (TextUtils.isEmpty(text)) {
"kavin.rocks (Official)"
} else {
text
}
}
}
}
}
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<String>) {
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()
}
}

View File

@ -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<String>
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
val region = findPreference<Preference>("region")
region?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true
}
val language = findPreference<ListPreference>("language")
language?.setOnPreferenceChangeListener { _, _ ->
ThemeHelper.restartMainActivity(requireContext())
val general = findPreference<Preference>("general")
general?.setOnPreferenceClickListener {
val newFragment = GeneralSettings()
navigateToSettingsFragment(newFragment)
true
}
@ -64,6 +55,20 @@ class MainSettings : PreferenceFragmentCompat() {
true
}
val history = findPreference<Preference>("history")
history?.setOnPreferenceClickListener {
val newFragment = HistorySettings()
navigateToSettingsFragment(newFragment)
true
}
val notifications = findPreference<Preference>("notifications")
notifications?.setOnPreferenceClickListener {
val newFragment = NotificationSettings()
navigateToSettingsFragment(newFragment)
true
}
val advanced = findPreference<Preference>("advanced")
advanced?.setOnPreferenceClickListener {
val newFragment = AdvancedSettings()
@ -72,29 +77,48 @@ class MainSettings : PreferenceFragmentCompat() {
}
val update = findPreference<Preference>("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
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()
}
val about = findPreference<Preference>("about")
about?.setOnPreferenceClickListener {
val newFragment = AboutFragment()
navigateToSettingsFragment(newFragment)
true
}
val community = findPreference<Preference>("community")
community?.setOnPreferenceClickListener {
val newFragment = CommunityFragment()
navigateToSettingsFragment(newFragment)
true
}
}
private fun navigateToSettingsFragment(newFragment: Fragment) {
Globals.isCurrentViewMainSettings = false
parentFragmentManager.beginTransaction()
.replace(R.id.settings, newFragment)
.commitNow()

View File

@ -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<SwitchPreferenceCompat>(PreferenceKeys.NOTIFICATION_ENABLED)
notificationsEnabled?.setOnPreferenceChangeListener { _, _ ->
updateNotificationPrefs()
true
}
val checkingFrequency = findPreference<ListPreference>(PreferenceKeys.CHECKING_FREQUENCY)
checkingFrequency?.setOnPreferenceChangeListener { _, _ ->
updateNotificationPrefs()
true
}
}
private fun updateNotificationPrefs() {
// replace the previous queued work request
NotificationHelper.enqueueWork(
requireContext(),
ExistingPeriodicWorkPolicy.REPLACE
)
}
}

View File

@ -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<ListPreference>("fullscreen_orientation")
val autoRotateToFullscreen = findPreference<SwitchPreferenceCompat>("auto_fullscreen")
val playerOrientation =
findPreference<ListPreference>(PreferenceKeys.FULLSCREEN_ORIENTATION)
val autoRotateToFullscreen =
findPreference<SwitchPreferenceCompat>(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<ListPreference>(PreferenceKeys.DEFAULT_SUBTITLE)
val locales: Array<Locale> = Locale.getAvailableLocales()
val localeNames = ArrayList<String>()
val localeCodes = ArrayList<String>()
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<ListPreference> { preference ->
preference.entry
}
}
}

View File

@ -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<CustomInstance> {
val settings = getDefaultSharedPreferences(context)
val gson = Gson()
fun getCustomInstances(): ArrayList<CustomInstance> {
val json: String = settings.getString("customInstances", "")!!
val type: Type = object : TypeToken<List<CustomInstance?>?>() {}.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<String> {
fun getSearchHistory(): List<String> {
return try {
val settings = getDefaultSharedPreferences(context)
val set: Set<String> = settings.getStringSet("search_history", HashSet())!!
set.toList()
val json = settings.getString("search_history", "")!!
val type = object : TypeReference<List<String>>() {}
return mapper.readValue(json, type)
} catch (e: Exception) {
emptyList()
}
}
fun saveHistory(context: Context, historyList: List<String>) {
val editor = getDefaultSharedPreferencesEditor(context)
val set: Set<String> = 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
}
fun addToWatchHistory(context: Context, videoId: String, streams: Streams) {
val editor = getDefaultSharedPreferencesEditor(context)
val gson = Gson()
// append new query to history
historyList.add(0, query)
if (historyList.size > 10) {
historyList.removeAt(historyList.size - 1)
}
updateSearchHistory(historyList)
}
fun removeFromSearchHistory(query: String) {
val historyList = getSearchHistory().toMutableList()
historyList -= query
updateSearchHistory(historyList)
}
private fun updateSearchHistory(historyList: List<String>) {
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<WatchHistoryItem> {
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<WatchHistoryItem> {
val json: String = settings.getString("watch_history", "")!!
val type: Type = object : TypeToken<List<WatchHistoryItem?>?>() {}.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<WatchPosition> {
val settings = getDefaultSharedPreferences(context)
val gson = Gson()
fun getWatchPositions(): ArrayList<WatchPosition> {
val json: String = settings.getString("watch_positions", "")!!
val type: Type = object : TypeToken<List<WatchPosition?>?>() {}.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<String> {
val json = settings.getString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, "")
return try {
val type = object : TypeReference<List<String>>() {}
mapper.readValue(json, type)
} catch (e: Exception) {
listOf()
}
}
fun setLocalSubscriptions(channels: List<String>) {
val json = mapper.writeValueAsString(channels)
editor.putString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, json).commit()
}
private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
private fun getDefaultSharedPreferencesEditor(context: Context): SharedPreferences.Editor {
return getDefaultSharedPreferences(context).edit()
}
}

View File

@ -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"
}

View File

@ -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?) {

View File

@ -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
}
}

View File

@ -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()

View File

@ -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))

View File

@ -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")
}
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -0,0 +1,27 @@
package com.github.libretube.update
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class UpdateInfo(
val assets: List<Asset>? = 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
)

View File

@ -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
)

View File

@ -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<String>()
private var playlistNextPage: String? = null
suspend fun getNextVideoId(
currentVideoId: String,
relatedStreams: List<StreamItem>
): 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<StreamItem>): 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
}
}

View File

@ -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)
}
}

Some files were not shown because too many files have changed in this diff Show More