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 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"> <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) [![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/assets/mat-widget.svg)](https://matrix.to/#/#LibreTube:matrix.org) [![Matrix](https://libre-tube.github.io/images/mat-widget.svg)](https://matrix.to/#/#LibreTube:matrix.org)
[![Telegram](https://libre-tube.github.io/assets/tg-widget.svg)](https://t.me/libretube) [![Telegram](https://libre-tube.github.io/images/tg-widget.svg)](https://t.me/libretube)
[![Twitter](https://libre-tube.github.io/assets/tw-widget.svg)](https://twitter.com/libretube) [![Twitter](https://libre-tube.github.io/images/tw-widget.svg)](https://twitter.com/libretube)
[![Reddit](https://libre-tube.github.io/assets/rd-widget.svg)](https://www.reddit.com/r/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;"> </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/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/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/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/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/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/assets/tgload.png" alt="Get it on GitHub" width="30%">](https://t.me/LibreTube) [<img src="https://libre-tube.github.io/images/tgload.png" alt="Get it on GitHub" width="30%">](https://t.me/LibreTube)
</div> </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 ## 📱 Screenshots
<div style="width:100%; display:flex; justify-content:space-between;"> <div style="width:100%; display:flex; justify-content:space-between;">
@ -47,14 +55,12 @@
## 😇 Contributing ## 😇 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 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. 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 ### 📝 Translation
<a href="https://hosted.weblate.org/projects/libretube/#languages"> <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://gitlab.com/libretube/LibreTube">GitLab</a></p>
<a href="https://notabug.org/LibreTube/LibreTube">NotABug</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 { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-android-extensions'
} }
android { android {
@ -10,8 +13,8 @@ android {
applicationId 'com.github.libretube' applicationId 'com.github.libretube'
minSdk 21 minSdk 21
targetSdk 31 targetSdk 31
versionCode 13 versionCode 16
versionName '0.3.3' versionName '0.4.2'
multiDexEnabled true multiDexEnabled true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
resValue "string", "app_name", "LibreTube" resValue "string", "app_name", "LibreTube"
@ -21,6 +24,16 @@ android {
viewBinding true 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 { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
@ -68,6 +81,7 @@ dependencies {
implementation libs.androidx.navigation.fragment implementation libs.androidx.navigation.fragment
implementation libs.androidx.navigation.ui implementation libs.androidx.navigation.ui
implementation libs.androidx.preference implementation libs.androidx.preference
implementation libs.androidx.work.runtime
androidTestImplementation libs.androidx.test.junit androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espressoCore 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.cronet) { exclude group: 'com.google.android.gms' }
implementation libs.exoplayer.extension.mediasession implementation libs.exoplayer.extension.mediasession
implementation libs.square.picasso
implementation libs.square.retrofit implementation libs.square.retrofit
implementation libs.square.retrofit.converterJackson implementation libs.square.retrofit.converterJackson
// Do not update jackson annotations! It does not supports < API 26. // Do not update jackson annotations! It does not supports < API 26.
implementation libs.jacksonAnnotations implementation libs.jacksonAnnotations
implementation libs.mobileffmpeg
coreLibraryDesugaring libs.desugaring coreLibraryDesugaring libs.desugaring
implementation libs.cronet.embedded 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 # Uncomment this to preserve the line number information for
# debugging stack traces. # debugging stack traces.
#-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
@ -22,3 +22,9 @@
#uncomment for debug #uncomment for debug
#-keepnames class ** #-keepnames class **
-keep class com.github.libretube.obj.** { *; } -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": [], "attributes": [],
"versionCode": 7, "versionCode": 16,
"versionName": "0.2.5", "versionName": "0.4.2",
"outputFile": "app-x86_64-release.apk" "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", "type": "ONE_OF_MANY",
"filters": [ "filters": [
@ -29,8 +42,8 @@
} }
], ],
"attributes": [], "attributes": [],
"versionCode": 7, "versionCode": 16,
"versionName": "0.2.5", "versionName": "0.4.2",
"outputFile": "app-x86-release.apk" "outputFile": "app-x86-release.apk"
}, },
{ {
@ -42,22 +55,9 @@
} }
], ],
"attributes": [], "attributes": [],
"versionCode": 7, "versionCode": 16,
"versionName": "0.2.5", "versionName": "0.4.2",
"outputFile": "app-armeabi-v7a-release.apk" "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" "elementType": "File"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"> android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" /> <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.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <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" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
@ -20,17 +23,25 @@
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Purple"> android:theme="@style/Theme.Purple.Pure"
<activity tools:targetApi="n">
android:name=".activities.Player"
android:configChanges="orientation|screenSize"
android:exported="false" />
<activity <activity
android:name=".activities.NoInternetActivity" android:name=".activities.NoInternetActivity"
android:label="@string/noInternet" /> android:label="@string/noInternet" />
<activity <activity
android:name=".activities.SettingsActivity" android:name=".activities.SettingsActivity"
android:label="@string/settings" /> android:label="@string/settings" />
<activity
android:name=".activities.AboutActivity"
android:label="@string/settings" />
<activity
android:name=".activities.CommunityActivity"
android:label="@string/settings" />
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
@ -281,8 +292,17 @@
<service <service
android:name=".services.ClosingService" android:name=".services.ClosingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false" />
android:stopWithTask="false" />
<service
android:name=".services.UpdateService"
android:enabled="true"
android:exported="false" />
<service
android:name=".services.BackgroundMode"
android:enabled="true"
android:exported="false" />
</application> </application>
</manifest> </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 DONATE_URL = "https://github.com/libre-tube/LibreTube#donate"
const val GITHUB_URL = "https://github.com/libre-tube/LibreTube" const val GITHUB_URL = "https://github.com/libre-tube/LibreTube"
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped" 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 * Social media links for the community fragment
@ -32,3 +33,19 @@ const val YOUTUBE_FRONTEND_URL = "https://www.youtube.com"
* Retrofit Instance * Retrofit Instance
*/ */
const val PIPED_API_URL = "https://pipedapi.kavin.rocks/" 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 package com.github.libretube
/**
* Global variables can be stored here
*/
object Globals { object Globals {
var isFullScreen = false // for the player fragment
var isMiniPlayerVisible = false var IS_FULL_SCREEN = false
var isCurrentViewMainSettings = true 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.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.os.Build 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() { class MyApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
/**
* initialize the needed [NotificationChannel]s for DownloadService and BackgroundMode
*/
initializeNotificationChannels() 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() { private fun initializeNotificationChannels() {
createNotificationChannel( createNotificationChannel(
"download_service", DOWNLOAD_CHANNEL_ID,
"Download Service", "Download Service",
"DownloadService", "Shows a notification when downloading media.",
NotificationManager.IMPORTANCE_NONE NotificationManager.IMPORTANCE_NONE
) )
createNotificationChannel( createNotificationChannel(
"background_mode", BACKGROUND_CHANNEL_ID,
"Background Mode", "Background Mode",
"Shows a notification with buttons to control the audio player", "Shows a notification with buttons to control the audio player",
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) )
createNotificationChannel(
PUSH_CHANNEL_ID,
"Notification Worker",
"Shows a notification when new streams are available.",
NotificationManager.IMPORTANCE_DEFAULT
)
} }
private fun createNotificationChannel( private fun createNotificationChannel(
@ -46,9 +109,4 @@ class MyApp : Application() {
notificationManager.createNotificationChannel(channel) 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.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Html 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.DONATE_URL
import com.github.libretube.GITHUB_URL import com.github.libretube.GITHUB_URL
import com.github.libretube.PIPED_GITHUB_URL import com.github.libretube.PIPED_GITHUB_URL
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.WEBLATE_URL
import com.github.libretube.WEBSITE_URL import com.github.libretube.WEBSITE_URL
import com.github.libretube.activities.SettingsActivity import com.github.libretube.databinding.ActivityAboutBinding
import com.github.libretube.databinding.FragmentAboutBinding import com.github.libretube.extensions.BaseActivity
import com.github.libretube.util.ThemeHelper.getThemeColor
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
class AboutFragment : Fragment() { class AboutActivity : BaseActivity() {
private lateinit var binding: FragmentAboutBinding private lateinit var binding: ActivityAboutBinding
override fun onCreateView( override fun onCreate(savedInstanceState: Bundle?) {
inflater: LayoutInflater, super.onCreate(savedInstanceState)
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentAboutBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ActivityAboutBinding.inflate(layoutInflater)
super.onViewCreated(view, savedInstanceState) setContentView(binding.root)
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.about))
binding.website.setOnClickListener { binding.website.setOnClickListener {
openLinkFromHref(WEBSITE_URL) openLinkFromHref(WEBSITE_URL)
} }
binding.website.setOnLongClickListener { binding.website.setOnLongClickListener {
val text = context?.getString(R.string.website_summary)!! val text = getString(R.string.website_summary)
showSnackBar(text) showSnackBar(text)
true true
} }
@ -51,7 +38,16 @@ class AboutFragment : Fragment() {
openLinkFromHref(PIPED_GITHUB_URL) openLinkFromHref(PIPED_GITHUB_URL)
} }
binding.piped.setOnLongClickListener { 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) showSnackBar(text)
true true
} }
@ -60,7 +56,7 @@ class AboutFragment : Fragment() {
openLinkFromHref(DONATE_URL) openLinkFromHref(DONATE_URL)
} }
binding.donate.setOnLongClickListener { binding.donate.setOnLongClickListener {
val text = context?.getString(R.string.donate_summary)!! val text = getString(R.string.donate_summary)
showSnackBar(text) showSnackBar(text)
true true
} }
@ -69,7 +65,7 @@ class AboutFragment : Fragment() {
openLinkFromHref(GITHUB_URL) openLinkFromHref(GITHUB_URL)
} }
binding.github.setOnLongClickListener { binding.github.setOnLongClickListener {
val text = context?.getString(R.string.contributing_summary)!! val text = getString(R.string.contributing_summary)
showSnackBar(text) showSnackBar(text)
true true
} }
@ -78,7 +74,7 @@ class AboutFragment : Fragment() {
showLicense() showLicense()
} }
binding.license.setOnLongClickListener { binding.license.setOnLongClickListener {
val text = context?.getString(R.string.license_summary)!! val text = getString(R.string.license_summary)
showSnackBar(text) showSnackBar(text)
true true
} }
@ -94,10 +90,6 @@ class AboutFragment : Fragment() {
val snackBar = Snackbar val snackBar = Snackbar
.make(binding.root, text, Snackbar.LENGTH_LONG) .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 // prevent the text from being partially hidden
snackBar.setTextMaxLines(3) snackBar.setTextMaxLines(3)
@ -105,7 +97,6 @@ class AboutFragment : Fragment() {
} }
private fun showLicense() { private fun showLicense() {
val assets = view?.context?.assets
val licenseString = assets val licenseString = assets
?.open("gpl3.html") ?.open("gpl3.html")
?.bufferedReader() ?.bufferedReader()
@ -119,7 +110,7 @@ class AboutFragment : Fragment() {
Html.fromHtml(licenseString.toString()) Html.fromHtml(licenseString.toString())
} }
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(this)
.setPositiveButton(getString(R.string.okay)) { _, _ -> } .setPositiveButton(getString(R.string.okay)) { _, _ -> }
.setMessage(licenseHtml) .setMessage(licenseHtml)
.create() .create()

View File

@ -1,38 +1,24 @@
package com.github.libretube.preferences package com.github.libretube.activities
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle 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.DISCORD_URL
import com.github.libretube.MATRIX_URL import com.github.libretube.MATRIX_URL
import com.github.libretube.R
import com.github.libretube.REDDIT_URL import com.github.libretube.REDDIT_URL
import com.github.libretube.TELEGRAM_URL import com.github.libretube.TELEGRAM_URL
import com.github.libretube.TWITTER_URL import com.github.libretube.TWITTER_URL
import com.github.libretube.activities.SettingsActivity import com.github.libretube.databinding.ActivityCommunityBinding
import com.github.libretube.databinding.FragmentCommunityBinding import com.github.libretube.extensions.BaseActivity
class CommunityFragment : Fragment() { class CommunityActivity : BaseActivity() {
private lateinit var binding: FragmentCommunityBinding private lateinit var binding: ActivityCommunityBinding
override fun onCreateView( override fun onCreate(savedInstanceState: Bundle?) {
inflater: LayoutInflater, super.onCreate(savedInstanceState)
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCommunityBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ActivityCommunityBinding.inflate(layoutInflater)
super.onViewCreated(view, savedInstanceState) setContentView(binding.root)
val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.community))
binding.telegram.setOnClickListener { binding.telegram.setOnClickListener {
openLinkFromHref(TELEGRAM_URL) openLinkFromHref(TELEGRAM_URL)

View File

@ -1,8 +1,7 @@
package com.github.libretube.activities package com.github.libretube.activities
import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -10,70 +9,81 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsetsController import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout 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.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import coil.ImageLoader
import com.github.libretube.Globals import com.github.libretube.Globals
import com.github.libretube.PIPED_API_URL
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.databinding.ActivityMainBinding 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.fragments.PlayerFragment
import com.github.libretube.models.SearchViewModel
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.services.ClosingService import com.github.libretube.services.ClosingService
import com.github.libretube.util.ConnectionHelper import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.CronetHelper import com.github.libretube.util.CronetHelper
import com.github.libretube.util.LocaleHelper import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.ThemeHelper 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.elevation.SurfaceColors
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
class MainActivity : AppCompatActivity() { class MainActivity : BaseActivity() {
val TAG = "MainActivity" val TAG = "MainActivity"
lateinit var binding: ActivityMainBinding lateinit var binding: ActivityMainBinding
lateinit var navController: NavController lateinit var navController: NavController
private var startFragmentId = R.id.homeFragment private var startFragmentId = R.id.homeFragment
var autoRotationEnabled = false
lateinit var searchView: SearchView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// set the app theme (e.g. Material You)
ThemeHelper.updateTheme(this)
// set the language // set the language
LocaleHelper.updateLanguage(this) LocaleHelper.updateLanguage(this)
super.onCreate(savedInstanceState) 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 // start service that gets called on closure
startService(Intent(this, ClosingService::class.java)) try {
startService(Intent(this, ClosingService::class.java))
} catch (e: Exception) {
e.printStackTrace()
}
CronetHelper.initCronet(this.applicationContext) CronetHelper.initCronet(this.applicationContext)
ConnectionHelper.imageLoader = ImageLoader.Builder(this.applicationContext)
.callFactory(CronetHelper.callFactory)
.build()
RetrofitInstance.url = // save whether the data saver mode is enabled
PreferenceHelper.getString(this, "selectInstance", PIPED_API_URL)!! Globals.DATA_SAVER_MODE_ENABLED = PreferenceHelper.getBoolean(
// set auth instance PreferenceKeys.DATA_SAVER_MODE,
RetrofitInstance.authUrl = false
if (PreferenceHelper.getBoolean(this, "auth_instance_toggle", false)) { )
PreferenceHelper.getString(
this,
"selectAuthInstance",
PIPED_API_URL
)!!
} else {
RetrofitInstance.url
}
// show noInternet Activity if no internet available on app startup // show noInternet Activity if no internet available on app startup
if (!ConnectionHelper.isNetworkAvailable(this)) { if (!ConnectionHelper.isNetworkAvailable(this)) {
@ -83,6 +93,9 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// set the action bar for the activity
setSupportActionBar(binding.toolbar)
navController = findNavController(R.id.fragment) navController = findNavController(R.id.fragment)
binding.bottomNav.setupWithNavController(navController) binding.bottomNav.setupWithNavController(navController)
@ -93,17 +106,19 @@ class MainActivity : AppCompatActivity() {
window.navigationBarColor = color window.navigationBarColor = color
// hide the trending page if enabled // 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 = if (hideTrendingPage) binding.bottomNav.menu.findItem(R.id.homeFragment).isVisible =
false false
// save start tab fragment id // save start tab fragment id
startFragmentId = when (PreferenceHelper.getString(this, "default_tab", "home")) { startFragmentId =
"home" -> R.id.homeFragment when (PreferenceHelper.getString(PreferenceKeys.DEFAULT_TAB, "home")) {
"subscriptions" -> R.id.subscriptionsFragment "home" -> R.id.homeFragment
"library" -> R.id.libraryFragment "subscriptions" -> R.id.subscriptionsFragment
else -> R.id.homeFragment "library" -> R.id.libraryFragment
} else -> R.id.homeFragment
}
// set default tab as start fragment // set default tab as start fragment
navController.graph.setStartDestination(startFragmentId) navController.graph.setStartDestination(startFragmentId)
@ -112,7 +127,7 @@ class MainActivity : AppCompatActivity() {
navController.navigate(startFragmentId) navController.navigate(startFragmentId)
val labelVisibilityMode = when ( val labelVisibilityMode = when (
PreferenceHelper.getString(this, "label_visibility", "always") PreferenceHelper.getString(PreferenceKeys.LABEL_VISIBILITY, "always")
) { ) {
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED "always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED "selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
@ -121,10 +136,13 @@ class MainActivity : AppCompatActivity() {
} }
binding.bottomNav.labelVisibilityMode = labelVisibilityMode binding.bottomNav.labelVisibilityMode = labelVisibilityMode
binding.bottomNav.setOnApplyWindowInsetsListener(null)
binding.bottomNav.setOnItemSelectedListener { binding.bottomNav.setOnItemSelectedListener {
// clear backstack if it's the start fragment // clear backstack if it's the start fragment
if (startFragmentId == it.itemId) navController.backQueue.clear() if (startFragmentId == it.itemId) navController.backQueue.clear()
// set menu item on click listeners // set menu item on click listeners
removeSearchFocus()
when (it.itemId) { when (it.itemId) {
R.id.homeFragment -> { R.id.homeFragment -> {
navController.navigate(R.id.homeFragment) navController.navigate(R.id.homeFragment)
@ -140,22 +158,140 @@ class MainActivity : AppCompatActivity() {
} }
binding.toolbar.title = ThemeHelper.getStyledAppName(this) binding.toolbar.title = ThemeHelper.getStyledAppName(this)
}
binding.toolbar.setNavigationOnClickListener { /**
// settings activity stuff * handle error logs
val intent = Intent(this, SettingsActivity::class.java) */
startActivity(intent) val log = PreferenceHelper.getErrorLog()
} if (log != "") ErrorDialog().show(supportFragmentManager, null)
binding.toolbar.setOnMenuItemClickListener { setupBreakReminder()
when (it.itemId) { }
R.id.action_search -> {
navController.navigate(R.id.searchFragment) /**
* Show a break reminder when watched too long
*/
private fun setupBreakReminder() {
val breakReminderPref = PreferenceHelper.getString(
PreferenceKeys.BREAK_REMINDER,
"disabled"
)
if (breakReminderPref == "disabled") return
Handler(Looper.getMainLooper()).postDelayed(
{
try {
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.share_with_time))
.setMessage(
getString(
R.string.already_spent_time,
breakReminderPref
)
)
.setPositiveButton(R.string.okay, null)
.show()
} catch (e: Exception) {
kotlin.runCatching {
Toast.makeText(this, R.string.take_a_break, Toast.LENGTH_LONG).show()
} }
} }
false },
breakReminderPref.toLong() * 60 * 1000
)
}
private fun removeSearchFocus() {
searchView.setQuery("", false)
searchView.clearFocus()
searchView.onActionViewCollapsed()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.action_bar, menu)
// stuff for the search in the topBar
val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView
val searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
searchView.setOnSearchClickListener {
if (navController.currentDestination?.id != R.id.searchResultFragment) {
searchViewModel.setQuery(null)
navController.navigate(R.id.searchFragment)
} }
} }
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
val bundle = Bundle()
bundle.putString("query", query)
navController.navigate(R.id.searchResultFragment, bundle)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
if (navController.currentDestination?.id != R.id.searchFragment) {
val bundle = Bundle()
bundle.putString("query", newText)
navController.navigate(R.id.searchFragment, bundle)
} else {
searchViewModel.setQuery(newText)
}
return true
}
})
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
return true
}
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
val currentFragmentId = navController.currentDestination?.id
if (currentFragmentId == R.id.searchFragment || currentFragmentId == R.id.searchResultFragment) {
onBackPressed()
}
return true
}
}
)
searchView.setOnCloseListener {
if (navController.currentDestination?.id == R.id.searchFragment) {
searchViewModel.setQuery(null)
onBackPressed()
}
false
}
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> {
val settingsIntent = Intent(this, SettingsActivity::class.java)
startActivity(settingsIntent)
true
}
R.id.action_about -> {
val aboutIntent = Intent(this, AboutActivity::class.java)
startActivity(aboutIntent)
true
}
R.id.action_community -> {
val communityIntent = Intent(this, CommunityActivity::class.java)
startActivity(communityIntent)
true
}
else -> super.onOptionsItemSelected(item)
}
} }
override fun onStart() { override fun onStart() {
@ -163,92 +299,95 @@ class MainActivity : AppCompatActivity() {
val intentData: Uri? = intent?.data val intentData: Uri? = intent?.data
// check whether an URI got submitted over the intent data // check whether an URI got submitted over the intent data
if (intentData != null && intentData.host != null && intentData.path != null) { 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) // load the URI of the submitted link (e.g. video)
loadIntentData(intentData) loadIntentData(intentData)
} }
} }
private fun loadIntentData(data: Uri) { 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("/c/") ||
data.path!!.contains("/user/") data.path!!.contains("/user/")
) { ) {
Log.i(TAG, "URI Type: Channel") val channelName = data.path!!
var channel = data.path .replace("/c/", "")
channel = channel!!.replace("/c/", "") .replace("/user/", "")
channel = channel.replace("/user/", "")
val bundle = bundleOf("channel_id" to channel) loadChannel(channelName = channelName)
navController.navigate(R.id.channelFragment, bundle) } else if (
} else if (data.path!!.contains("/playlist")) { data.path!!.contains("/playlist")
Log.i(TAG, "URI Type: Playlist") ) {
var playlist = data.query!! var playlistId = data.query!!
if (playlist.contains("&")) { if (playlistId.contains("&")) {
val playlists = playlist.split("&") for (v in playlistId.split("&")) {
for (v in playlists) {
if (v.contains("list=")) { if (v.contains("list=")) {
playlist = v playlistId = v.replace("list=", "")
break break
} }
} }
} else {
playlistId = playlistId.replace("list=", "")
} }
playlist = playlist.replace("list=", "")
val bundle = bundleOf("playlist_id" to playlist) loadPlaylist(playlistId)
navController.navigate(R.id.playlistFragment, bundle) } else if (
} else if (data.path!!.contains("/shorts/") || data.path!!.contains("/shorts/") ||
data.path!!.contains("/embed/") || data.path!!.contains("/embed/") ||
data.path!!.contains("/v/") data.path!!.contains("/v/")
) { ) {
Log.i(TAG, "URI Type: Video") val videoId = data.path!!
val watch = data.path!!
.replace("/shorts/", "") .replace("/shorts/", "")
.replace("/v/", "") .replace("/v/", "")
.replace("/embed/", "") .replace("/embed/", "")
val bundle = Bundle()
bundle.putString("videoId", watch) loadVideo(videoId, data.query)
// 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 if (data.path!!.contains("/watch") && data.query != null) { } else if (data.path!!.contains("/watch") && data.query != null) {
Log.d("dafaq", data.query!!) var videoId = data.query!!
var watch = data.query!!
if (watch.contains("&")) { if (videoId.contains("&")) {
val watches = watch.split("&") val watches = videoId.split("&")
for (v in watches) { for (v in watches) {
if (v.contains("v=")) { if (v.contains("v=")) {
watch = v videoId = v.replace("v=", "")
break break
} }
} }
} else {
videoId = videoId
.replace("v=", "")
} }
val bundle = Bundle()
bundle.putString("videoId", watch.replace("v=", "")) loadVideo(videoId, data.query)
// 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 { } else {
val watch = data.path!!.replace("/", "") val videoId = data.path!!.replace("/", "")
val bundle = Bundle()
bundle.putString("videoId", watch) loadVideo(videoId, data.query)
// 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)
} }
} }
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() val frag = PlayerFragment()
frag.arguments = bundle frag.arguments = bundle
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.remove(PlayerFragment()) .remove(PlayerFragment())
.commit() .commit()
@ -262,13 +401,36 @@ class MainActivity : AppCompatActivity() {
}, 100) }, 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() { override fun onBackPressed() {
// remove focus from search
removeSearchFocus()
navController.popBackStack(R.id.searchFragment, false)
if (binding.mainMotionLayout.progress == 0F) { if (binding.mainMotionLayout.progress == 0F) {
try { try {
minimizePlayer() minimizePlayer()
} catch (e: Exception) { } catch (e: Exception) {
if (navController.currentDestination?.id == startFragmentId) { if (navController.currentDestination?.id == startFragmentId) {
super.onBackPressed() // close app
moveTaskToBack(true)
} else { } else {
navController.popBackStack() navController.popBackStack()
} }
@ -292,7 +454,9 @@ class MainActivity : AppCompatActivity() {
enableTransition(R.id.yt_transition, true) enableTransition(R.id.yt_transition, true)
} }
findViewById<LinearLayout>(R.id.linLayout).visibility = View.VISIBLE 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) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -330,9 +494,13 @@ class MainActivity : AppCompatActivity() {
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 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() { private fun unsetFullscreen() {
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode = window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 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.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.databinding.ActivityNointernetBinding import com.github.libretube.databinding.ActivityNointernetBinding
import com.github.libretube.extensions.BaseActivity
import com.github.libretube.util.ConnectionHelper import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
class NoInternetActivity : AppCompatActivity() { class NoInternetActivity : BaseActivity() {
private lateinit var binding: ActivityNointernetBinding private lateinit var binding: ActivityNointernetBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.updateTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityNointernetBinding.inflate(layoutInflater) binding = ActivityNointernetBinding.inflate(layoutInflater)

View File

@ -5,11 +5,11 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.extensions.BaseActivity
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
class RouterActivity : AppCompatActivity() { class RouterActivity : BaseActivity() {
val TAG = "RouterActivity" val TAG = "RouterActivity"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -1,32 +1,19 @@
package com.github.libretube.activities package com.github.libretube.activities
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.Globals
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.databinding.ActivitySettingsBinding import com.github.libretube.databinding.ActivitySettingsBinding
import com.github.libretube.extensions.BaseActivity
import com.github.libretube.preferences.MainSettings import com.github.libretube.preferences.MainSettings
import com.github.libretube.util.ThemeHelper
class SettingsActivity : AppCompatActivity() { class SettingsActivity : BaseActivity() {
val TAG = "SettingsActivity" val TAG = "SettingsActivity"
lateinit var binding: ActivitySettingsBinding lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.updateTheme(this)
// apply the theme for the preference dialogs
setTheme(R.style.MaterialAlertDialog)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater) 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) setContentView(binding.root)
@ -43,16 +30,18 @@ class SettingsActivity : AppCompatActivity() {
} }
override fun onBackPressed() { override fun onBackPressed() {
if (Globals.isCurrentViewMainSettings) { when (supportFragmentManager.findFragmentById(R.id.settings)) {
super.onBackPressed() is MainSettings -> {
finishAndRemoveTask() super.onBackPressed()
} else { finishAndRemoveTask()
Globals.isCurrentViewMainSettings = true }
supportFragmentManager else -> {
.beginTransaction() supportFragmentManager
.replace(R.id.settings, MainSettings()) .beginTransaction()
.commit() .replace(R.id.settings, MainSettings())
changeTopBarText(getString(R.string.settings)) .commit()
changeTopBarText(getString(R.string.settings))
}
} }
} }

View File

@ -1,19 +1,18 @@
package com.github.libretube.adapters package com.github.libretube.adapters
import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.databinding.VideoChannelRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog import com.github.libretube.dialogs.VideoOptionsDialog
import com.github.libretube.fragments.PlayerFragment
import com.github.libretube.obj.StreamItem 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.github.libretube.util.formatShort
import com.squareup.picasso.Picasso import com.github.libretube.util.setWatchProgressLength
import com.github.libretube.util.toID
class ChannelAdapter( class ChannelAdapter(
private val videoFeed: MutableList<StreamItem>, private val videoFeed: MutableList<StreamItem>,
@ -26,47 +25,39 @@ class ChannelAdapter(
} }
fun updateItems(newItems: List<StreamItem>) { fun updateItems(newItems: List<StreamItem>) {
val feedSize = videoFeed.size
videoFeed.addAll(newItems) videoFeed.addAll(newItems)
notifyDataSetChanged() notifyItemRangeInserted(feedSize, newItems.size)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
val binding = VideoChannelRowBinding.inflate(layoutInflater, parent, false) val binding = VideoRowBinding.inflate(layoutInflater, parent, false)
return ChannelViewHolder(binding) return ChannelViewHolder(binding)
} }
override fun onBindViewHolder(holder: ChannelViewHolder, position: Int) { override fun onBindViewHolder(holder: ChannelViewHolder, position: Int) {
val trending = videoFeed[position] val trending = videoFeed[position]
holder.binding.apply { holder.binding.apply {
channelDescription.text = trending.title videoTitle.text = trending.title
channelViews.text = videoInfo.text =
trending.views.formatShort() + "" + trending.views.formatShort() + "" +
DateUtils.getRelativeTimeSpanString(trending.uploaded!!) DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
channelDuration.text = thumbnailDuration.text =
DateUtils.formatElapsedTime(trending.duration!!) DateUtils.formatElapsedTime(trending.duration!!)
Picasso.get().load(trending.thumbnail).into(channelThumbnail) ConnectionHelper.loadImage(trending.thumbnail, thumbnail)
root.setOnClickListener { root.setOnClickListener {
var bundle = Bundle() NavigationHelper.navigateVideo(root.context, trending.url)
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()
} }
val videoId = trending.url.toID()
root.setOnLongClickListener { root.setOnLongClickListener {
val videoId = trending.url!!.replace("/watch?v=", "") VideoOptionsDialog(videoId)
VideoOptionsDialog(videoId, root.context) .show(childFragmentManager, VideoOptionsDialog::class.java.name)
.show(childFragmentManager, VideoOptionsDialog.TAG)
true 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 package com.github.libretube.adapters
import android.graphics.Color
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.ChapterColumnBinding import com.github.libretube.databinding.ChapterColumnBinding
import com.github.libretube.obj.ChapterSegment 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.google.android.exoplayer2.ExoPlayer
import com.squareup.picasso.Picasso
class ChaptersAdapter( class ChaptersAdapter(
private val chapters: List<ChapterSegment>, private val chapters: List<ChapterSegment>,
private val exoPlayer: ExoPlayer private val exoPlayer: ExoPlayer
) : RecyclerView.Adapter<ChaptersViewHolder>() { ) : RecyclerView.Adapter<ChaptersViewHolder>() {
val TAG = "ChaptersAdapter" val TAG = "ChaptersAdapter"
private var selectedPosition = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
@ -23,16 +26,30 @@ class ChaptersAdapter(
override fun onBindViewHolder(holder: ChaptersViewHolder, position: Int) { override fun onBindViewHolder(holder: ChaptersViewHolder, position: Int) {
val chapter = chapters[position] val chapter = chapters[position]
holder.binding.apply { holder.binding.apply {
Picasso.get().load(chapter.image).fit().centerCrop().into(chapterImage) ConnectionHelper.loadImage(chapter.image, chapterImage)
chapterTitle.text = chapter.title 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 { root.setOnClickListener {
updateSelectedPosition(position)
val chapterStart = chapter.start!! * 1000 // s -> ms val chapterStart = chapter.start!! * 1000 // s -> ms
exoPlayer.seekTo(chapterStart) exoPlayer.seekTo(chapterStart)
} }
} }
} }
fun updateSelectedPosition(newPosition: Int) {
val oldPosition = selectedPosition
selectedPosition = newPosition
notifyItemChanged(oldPosition)
notifyItemChanged(newPosition)
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
return chapters.size return chapters.size
} }

View File

@ -5,18 +5,16 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.CommentsRowBinding import com.github.libretube.databinding.CommentsRowBinding
import com.github.libretube.obj.Comment import com.github.libretube.obj.Comment
import com.github.libretube.obj.CommentsPage 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.RetrofitInstance
import com.github.libretube.util.formatShort import com.github.libretube.util.formatShort
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -53,7 +51,7 @@ class CommentsAdapter(
"" + comment.commentedTime.toString() "" + comment.commentedTime.toString()
commentText.text = commentText.text =
comment.commentText.toString() comment.commentText.toString()
Picasso.get().load(comment.thumbnail).fit().centerCrop().into(commentorImage) ConnectionHelper.loadImage(comment.thumbnail, commentorImage)
likesTextView.text = likesTextView.text =
comment.likeCount?.toLong().formatShort() comment.likeCount?.toLong().formatShort()
if (comment.verified == true) { if (comment.verified == true) {
@ -66,19 +64,7 @@ class CommentsAdapter(
heartedImageView.visibility = View.VISIBLE heartedImageView.visibility = View.VISIBLE
} }
commentorImage.setOnClickListener { commentorImage.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
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) {
}
} }
repliesRecView.layoutManager = LinearLayoutManager(root.context) repliesRecView.layoutManager = LinearLayoutManager(root.context)
val repliesAdapter = RepliesAdapter(CommentsPage().comments) val repliesAdapter = RepliesAdapter(CommentsPage().comments)

View File

@ -1,24 +1,23 @@
package com.github.libretube.adapters package com.github.libretube.adapters
import android.app.Activity import android.app.Activity
import android.os.Bundle
import android.text.format.DateUtils
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.databinding.PlaylistRowBinding import com.github.libretube.databinding.PlaylistRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog 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.PlaylistId
import com.github.libretube.obj.StreamItem import com.github.libretube.obj.StreamItem
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -54,69 +53,46 @@ class PlaylistAdapter(
holder.binding.apply { holder.binding.apply {
playlistTitle.text = streamItem.title playlistTitle.text = streamItem.title
playlistDescription.text = streamItem.uploaderName playlistDescription.text = streamItem.uploaderName
playlistDuration.text = DateUtils.formatElapsedTime(streamItem.duration!!) thumbnailDuration.setFormattedDuration(streamItem.duration!!)
Picasso.get().load(streamItem.thumbnail).into(playlistThumbnail) ConnectionHelper.loadImage(streamItem.thumbnail, playlistThumbnail)
root.setOnClickListener { root.setOnClickListener {
var bundle = Bundle() NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)
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()
} }
val videoId = streamItem.url.toID()
root.setOnLongClickListener { root.setOnLongClickListener {
val videoId = streamItem.url!!.replace("/watch?v=", "") VideoOptionsDialog(videoId)
VideoOptionsDialog(videoId, root.context) .show(childFragmentManager, VideoOptionsDialog::class.java.name)
.show(childFragmentManager, VideoOptionsDialog.TAG)
true true
} }
if (isOwner) { if (isOwner) {
deletePlaylist.visibility = View.VISIBLE deletePlaylist.visibility = View.VISIBLE
deletePlaylist.setOnClickListener { deletePlaylist.setOnClickListener {
val token = PreferenceHelper.getToken(root.context) removeFromPlaylist(position)
removeFromPlaylist(token, position)
} }
} }
watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
} }
} }
private fun removeFromPlaylist(token: String, position: Int) { fun removeFromPlaylist(position: Int) {
fun run() { videoFeed.removeAt(position)
CoroutineScope(Dispatchers.IO).launch { activity.runOnUiThread { notifyDataSetChanged() }
val response = try { CoroutineScope(Dispatchers.IO).launch {
RetrofitInstance.authApi.removeFromPlaylist( try {
token, RetrofitInstance.authApi.removeFromPlaylist(
PlaylistId(playlistId = playlistId, index = position) PreferenceHelper.getToken(),
) PlaylistId(playlistId = playlistId, index = position)
} catch (e: IOException) { )
println(e) } catch (e: IOException) {
Log.e(TAG, "IOException, you might not have internet connection") println(e)
return@launch Log.e(TAG, "IOException, you might not have internet connection")
} catch (e: HttpException) { return@launch
Log.e(TAG, "HttpException, unexpected response") } catch (e: HttpException) {
return@launch Log.e(TAG, "HttpException, unexpected response")
} finally { return@launch
}
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()
} }
} }

View File

@ -4,17 +4,18 @@ import android.app.Activity
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.PlaylistsRowBinding import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.dialogs.PlaylistOptionsDialog
import com.github.libretube.obj.PlaylistId import com.github.libretube.obj.PlaylistId
import com.github.libretube.obj.Playlists import com.github.libretube.obj.Playlists
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -23,6 +24,7 @@ import java.io.IOException
class PlaylistsAdapter( class PlaylistsAdapter(
private val playlists: MutableList<Playlists>, private val playlists: MutableList<Playlists>,
private val childFragmentManager: FragmentManager,
private val activity: Activity private val activity: Activity
) : RecyclerView.Adapter<PlaylistsViewHolder>() { ) : RecyclerView.Adapter<PlaylistsViewHolder>() {
val TAG = "PlaylistsAdapter" val TAG = "PlaylistsAdapter"
@ -45,11 +47,12 @@ class PlaylistsAdapter(
override fun onBindViewHolder(holder: PlaylistsViewHolder, position: Int) { override fun onBindViewHolder(holder: PlaylistsViewHolder, position: Int) {
val playlist = playlists[position] val playlist = playlists[position]
holder.binding.apply { holder.binding.apply {
Picasso.get().load(playlist.thumbnail).into(playlistThumbnail)
// set imageview drawable as empty playlist if imageview empty // 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.setImageResource(R.drawable.ic_empty_playlist)
playlistThumbnail.setBackgroundColor(R.attr.colorSurface) playlistThumbnail.setBackgroundColor(R.attr.colorSurface)
} else {
ConnectionHelper.loadImage(playlist.thumbnail, playlistThumbnail)
} }
playlistTitle.text = playlist.name playlistTitle.text = playlist.name
deletePlaylist.setOnClickListener { deletePlaylist.setOnClickListener {
@ -57,27 +60,38 @@ class PlaylistsAdapter(
builder.setTitle(R.string.deletePlaylist) builder.setTitle(R.string.deletePlaylist)
builder.setMessage(R.string.areYouSure) builder.setMessage(R.string.areYouSure)
builder.setPositiveButton(R.string.yes) { _, _ -> builder.setPositiveButton(R.string.yes) { _, _ ->
val token = PreferenceHelper.getToken(root.context) PreferenceHelper.getToken()
deletePlaylist(playlist.id!!, token, position) deletePlaylist(playlist.id!!, position)
}
builder.setNegativeButton(R.string.cancel) { _, _ ->
} }
builder.setNegativeButton(R.string.cancel, null)
builder.show() builder.show()
} }
root.setOnClickListener { root.setOnClickListener {
// playlists clicked NavigationHelper.navigatePlaylist(root.context, playlist.id, true)
val activity = root.context as MainActivity }
val bundle = bundleOf("playlist_id" to playlist.id)
activity.navController.navigate(R.id.playlistFragment, bundle) 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() { fun run() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = try { val response = try {
RetrofitInstance.authApi.deletePlaylist(token, PlaylistId(id)) RetrofitInstance.authApi.deletePlaylist(
PreferenceHelper.getToken(),
PlaylistId(id)
)
} catch (e: IOException) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG, "IOException, you might not have internet connection") Log.e(TAG, "IOException, you might not have internet connection")
@ -88,9 +102,7 @@ class PlaylistsAdapter(
} }
try { try {
if (response.message == "ok") { if (response.message == "ok") {
Log.d(TAG, "deleted!")
playlists.removeAt(position) playlists.removeAt(position)
// FIXME: This needs to run on UI thread?
activity.runOnUiThread { notifyDataSetChanged() } activity.runOnUiThread { notifyDataSetChanged() }
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -3,15 +3,12 @@ package com.github.libretube.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView 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.databinding.RepliesRowBinding
import com.github.libretube.obj.Comment 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.github.libretube.util.formatShort
import com.squareup.picasso.Picasso
class RepliesAdapter( class RepliesAdapter(
private val replies: MutableList<Comment> private val replies: MutableList<Comment>
@ -44,7 +41,7 @@ class RepliesAdapter(
"" + reply.commentedTime.toString() "" + reply.commentedTime.toString()
commentText.text = commentText.text =
reply.commentText.toString() reply.commentText.toString()
Picasso.get().load(reply.thumbnail).fit().centerCrop().into(commentorImage) ConnectionHelper.loadImage(reply.thumbnail, commentorImage)
likesTextView.text = likesTextView.text =
reply.likeCount?.toLong().formatShort() reply.likeCount?.toLong().formatShort()
if (reply.verified == true) { if (reply.verified == true) {
@ -57,19 +54,7 @@ class RepliesAdapter(
heartedImageView.visibility = View.VISIBLE heartedImageView.visibility = View.VISIBLE
} }
commentorImage.setOnClickListener { commentorImage.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateVideo(root.context, reply.commentorUrl)
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) {
}
} }
} }
} }

View File

@ -1,32 +1,27 @@
package com.github.libretube.adapters package com.github.libretube.adapters
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.MainActivity import com.github.libretube.databinding.ChannelRowBinding
import com.github.libretube.databinding.ChannelSearchRowBinding
import com.github.libretube.databinding.PlaylistSearchRowBinding 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.PlaylistOptionsDialog
import com.github.libretube.dialogs.VideoOptionsDialog 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.SearchItem
import com.github.libretube.obj.Subscribe import com.github.libretube.util.ConnectionHelper
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort import com.github.libretube.util.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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException
class SearchAdapter( class SearchAdapter(
private val searchItems: MutableList<SearchItem>, private val searchItems: MutableList<SearchItem>,
@ -49,10 +44,10 @@ class SearchAdapter(
return when (viewType) { return when (viewType) {
0 -> SearchViewHolder( 0 -> SearchViewHolder(
VideoSearchRowBinding.inflate(layoutInflater, parent, false) VideoRowBinding.inflate(layoutInflater, parent, false)
) )
1 -> SearchViewHolder( 1 -> SearchViewHolder(
ChannelSearchRowBinding.inflate(layoutInflater, parent, false) ChannelRowBinding.inflate(layoutInflater, parent, false)
) )
2 -> SearchViewHolder( 2 -> SearchViewHolder(
PlaylistSearchRowBinding.inflate(layoutInflater, parent, false) PlaylistSearchRowBinding.inflate(layoutInflater, parent, false)
@ -82,161 +77,101 @@ class SearchAdapter(
} }
} }
private fun bindWatch(item: SearchItem, binding: VideoSearchRowBinding) { private fun bindWatch(item: SearchItem, binding: VideoRowBinding) {
binding.apply { binding.apply {
Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchThumbnail) ConnectionHelper.loadImage(item.thumbnail, thumbnail)
if (item.duration != -1L) { thumbnailDuration.setFormattedDuration(item.duration!!)
searchThumbnailDuration.text = DateUtils.formatElapsedTime(item.duration!!) ConnectionHelper.loadImage(item.uploaderAvatar, channelImage)
} else { videoTitle.text = item.title
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
val viewsString = if (item.views?.toInt() != -1) item.views.formatShort() else "" val viewsString = if (item.views?.toInt() != -1) item.views.formatShort() else ""
val uploadDate = if (item.uploadedDate != null) item.uploadedDate else "" val uploadDate = if (item.uploadedDate != null) item.uploadedDate else ""
searchViews.text = videoInfo.text =
if (viewsString != "" && uploadDate != "") { if (viewsString != "" && uploadDate != "") {
"$viewsString$uploadDate" "$viewsString$uploadDate"
} else { } else {
viewsString + uploadDate viewsString + uploadDate
} }
searchChannelName.text = item.uploaderName channelName.text = item.uploaderName
root.setOnClickListener { root.setOnClickListener {
val bundle = Bundle() NavigationHelper.navigateVideo(root.context, item.url)
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()
} }
val videoId = item.url.toID()
root.setOnLongClickListener { root.setOnLongClickListener {
val videoId = item.url!!.replace("/watch?v=", "") VideoOptionsDialog(videoId)
VideoOptionsDialog(videoId, root.context) .show(childFragmentManager, VideoOptionsDialog::class.java.name)
.show(childFragmentManager, VideoOptionsDialog.TAG)
true true
} }
searchChannelImage.setOnClickListener { channelImage.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
val bundle = bundleOf("channel_id" to item.uploaderUrl)
activity.navController.navigate(R.id.channelFragment, bundle)
} }
watchProgress.setWatchProgressLength(videoId, item.duration!!)
} }
} }
private fun bindChannel(item: SearchItem, binding: ChannelSearchRowBinding) { private fun bindChannel(item: SearchItem, binding: ChannelRowBinding) {
binding.apply { binding.apply {
Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchChannelImage) ConnectionHelper.loadImage(item.thumbnail, searchChannelImage)
searchChannelName.text = item.name searchChannelName.text = item.name
searchViews.text = root.context.getString( searchViews.text = root.context.getString(
R.string.subscribers, R.string.subscribers,
item.subscribers.formatShort() item.subscribers.formatShort()
) + "" + root.context.getString(R.string.videoCount, item.videos.toString()) ) + "" + root.context.getString(R.string.videoCount, item.videos.toString())
root.setOnClickListener { root.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateChannel(root.context, item.url)
val bundle = bundleOf("channel_id" to item.url)
activity.navController.navigate(R.id.channelFragment, bundle)
} }
val channelId = item.url?.replace("/channel/", "")!! val channelId = item.url.toID()
val token = PreferenceHelper.getToken(root.context)
// only show subscribe button if logged in isSubscribed(channelId, binding)
if (token != "") isSubscribed(channelId, token, binding)
} }
} }
private fun isSubscribed(channelId: String, token: String, binding: ChannelSearchRowBinding) { private fun isSubscribed(channelId: String, binding: ChannelRowBinding) {
var isSubscribed = false
// check whether the user subscribed to the channel // check whether the user subscribed to the channel
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = try { var isSubscribed = SubscriptionHelper.isSubscribed(channelId)
RetrofitInstance.authApi.isSubscribed(
channelId,
token
)
} catch (e: Exception) {
return@launch
}
// if subscribed change text to unsubscribe // if subscribed change text to unsubscribe
if (response.subscribed == true) { if (isSubscribed == true) {
isSubscribed = true
binding.searchSubButton.text = binding.root.context.getString(R.string.unsubscribe) binding.searchSubButton.text = binding.root.context.getString(R.string.unsubscribe)
} }
// make sub button visible and set the on click listeners to (un)subscribe // 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.visibility = View.VISIBLE
binding.searchSubButton.setOnClickListener { binding.searchSubButton.setOnClickListener {
if (!isSubscribed) { if (isSubscribed == false) {
subscribe(token, channelId) SubscriptionHelper.subscribe(channelId)
binding.searchSubButton.text = binding.searchSubButton.text =
binding.root.context.getString(R.string.unsubscribe) binding.root.context.getString(R.string.unsubscribe)
isSubscribed = true isSubscribed = true
} else { } else {
unsubscribe(token, channelId) SubscriptionHelper.unsubscribe(channelId)
binding.searchSubButton.text = binding.searchSubButton.text =
binding.root.context.getString(R.string.subscribe) binding.root.context.getString(R.string.subscribe)
isSubscribed = false isSubscribed = false
}
} }
} }
} }
} }
private fun subscribe(token: String, channelId: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.subscribe(
token,
Subscribe(channelId)
)
} catch (e: Exception) {
return@launch
}
}
}
private fun unsubscribe(token: String, channelId: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.unsubscribe(
token,
Subscribe(channelId)
)
} catch (e: IOException) {
return@launch
}
}
}
private fun bindPlaylist(item: SearchItem, binding: PlaylistSearchRowBinding) { private fun bindPlaylist(item: SearchItem, binding: PlaylistSearchRowBinding) {
binding.apply { 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() if (item.videos?.toInt() != -1) searchPlaylistNumber.text = item.videos.toString()
searchDescription.text = item.name searchDescription.text = item.name
searchName.text = item.uploaderName searchName.text = item.uploaderName
if (item.videos?.toInt() != -1) { if (item.videos?.toInt() != -1) {
searchPlaylistNumber.text = searchPlaylistVideos.text =
root.context.getString(R.string.videoCount, item.videos.toString()) root.context.getString(R.string.videoCount, item.videos.toString())
} }
root.setOnClickListener { root.setOnClickListener {
// playlist clicked NavigationHelper.navigatePlaylist(root.context, item.url, false)
val activity = root.context as MainActivity
val bundle = bundleOf("playlist_id" to item.url)
activity.navController.navigate(R.id.playlistFragment, bundle)
} }
root.setOnLongClickListener { root.setOnLongClickListener {
val playlistId = item.url!!.replace("/playlist?list=", "") val playlistId = item.url!!.toID()
PlaylistOptionsDialog(playlistId, false, root.context) PlaylistOptionsDialog(playlistId, false)
.show(childFragmentManager, "PlaylistOptionsDialog") .show(childFragmentManager, PlaylistOptionsDialog::class.java.name)
true true
} }
} }
@ -244,15 +179,15 @@ class SearchAdapter(
} }
class SearchViewHolder : RecyclerView.ViewHolder { class SearchViewHolder : RecyclerView.ViewHolder {
var videoRowBinding: VideoSearchRowBinding? = null var videoRowBinding: VideoRowBinding? = null
var channelRowBinding: ChannelSearchRowBinding? = null var channelRowBinding: ChannelRowBinding? = null
var playlistRowBinding: PlaylistSearchRowBinding? = null var playlistRowBinding: PlaylistSearchRowBinding? = null
constructor(binding: VideoSearchRowBinding) : super(binding.root) { constructor(binding: VideoRowBinding) : super(binding.root) {
videoRowBinding = binding videoRowBinding = binding
} }
constructor(binding: ChannelSearchRowBinding) : super(binding.root) { constructor(binding: ChannelRowBinding) : super(binding.root) {
channelRowBinding = binding channelRowBinding = binding
} }

View File

@ -1,19 +1,15 @@
package com.github.libretube.adapters package com.github.libretube.adapters
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.SearchhistoryRowBinding import com.github.libretube.databinding.SearchhistoryRowBinding
import com.github.libretube.fragments.SearchFragment
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
class SearchHistoryAdapter( class SearchHistoryAdapter(
private val context: Context,
private var historyList: List<String>, private var historyList: List<String>,
private val editText: EditText, private val searchView: SearchView
private val searchFragment: SearchFragment
) : ) :
RecyclerView.Adapter<SearchHistoryViewHolder>() { RecyclerView.Adapter<SearchHistoryViewHolder>() {
@ -28,19 +24,18 @@ class SearchHistoryAdapter(
} }
override fun onBindViewHolder(holder: SearchHistoryViewHolder, position: Int) { override fun onBindViewHolder(holder: SearchHistoryViewHolder, position: Int) {
val history = historyList[position] val historyQuery = historyList[position]
holder.binding.apply { holder.binding.apply {
historyText.text = history historyText.text = historyQuery
deleteHistory.setOnClickListener { deleteHistory.setOnClickListener {
historyList = historyList - history historyList = historyList - historyQuery
PreferenceHelper.saveHistory(context, historyList) PreferenceHelper.removeFromSearchHistory(historyQuery)
notifyDataSetChanged() notifyDataSetChanged()
} }
root.setOnClickListener { root.setOnClickListener {
editText.setText(history) searchView.setQuery(historyQuery, true)
searchFragment.fetchSearch(history)
} }
} }
} }

View File

@ -2,15 +2,13 @@ package com.github.libretube.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.SearchsuggestionRowBinding import com.github.libretube.databinding.SearchsuggestionRowBinding
import com.github.libretube.fragments.SearchFragment
class SearchSuggestionsAdapter( class SearchSuggestionsAdapter(
private var suggestionsList: List<String>, private var suggestionsList: List<String>,
private var editText: EditText, private val searchView: SearchView
private val searchFragment: SearchFragment
) : ) :
RecyclerView.Adapter<SearchSuggestionsViewHolder>() { RecyclerView.Adapter<SearchSuggestionsViewHolder>() {
@ -31,8 +29,7 @@ class SearchSuggestionsAdapter(
holder.binding.apply { holder.binding.apply {
suggestionText.text = suggestion suggestionText.text = suggestion
root.setOnClickListener { root.setOnClickListener {
editText.setText(suggestion) searchView.setQuery(suggestion, true)
searchFragment.fetchSearch(editText.text.toString())
} }
} }
} }

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 package com.github.libretube.adapters
import android.content.Context
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.ChannelSubscriptionRowBinding import com.github.libretube.databinding.ChannelSubscriptionRowBinding
import com.github.libretube.obj.Subscribe
import com.github.libretube.obj.Subscription import com.github.libretube.obj.Subscription
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.NavigationHelper
import com.squareup.picasso.Picasso import com.github.libretube.util.SubscriptionHelper
import kotlinx.coroutines.CoroutineScope import com.github.libretube.util.toID
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscription>) : class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscription>) :
RecyclerView.Adapter<SubscriptionChannelViewHolder>() { RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
val TAG = "SubChannelAdapter" val TAG = "SubChannelAdapter"
private var subscribed = true
private var isLoading = false
override fun getItemCount(): Int { override fun getItemCount(): Int {
return subscriptions.size return subscriptions.size
} }
@ -38,68 +28,29 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscrip
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) { override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
val subscription = subscriptions[position] val subscription = subscriptions[position]
var subscribed = true
holder.binding.apply { holder.binding.apply {
subscriptionChannelName.text = subscription.name subscriptionChannelName.text = subscription.name
Picasso.get().load(subscription.avatar).into(subscriptionChannelImage) ConnectionHelper.loadImage(subscription.avatar, subscriptionChannelImage)
root.setOnClickListener { root.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateChannel(root.context, subscription.url)
val bundle = bundleOf("channel_id" to subscription.url)
activity.navController.navigate(R.id.channelFragment, bundle)
} }
subscriptionSubscribe.setOnClickListener { subscriptionSubscribe.setOnClickListener {
if (!isLoading) { val channelId = subscription.url.toID()
isLoading = true if (subscribed) {
val channelId = subscription.url?.replace("/channel/", "")!! subscriptionSubscribe.text = root.context.getString(R.string.subscribe)
if (subscribed) { SubscriptionHelper.unsubscribe(channelId)
unsubscribe(root.context, channelId) subscribed = false
subscriptionSubscribe.text = root.context.getString(R.string.subscribe) } else {
} else { subscriptionSubscribe.text =
subscribe(root.context, channelId) root.context.getString(R.string.unsubscribe)
subscriptionSubscribe.text = SubscriptionHelper.subscribe(channelId)
root.context.getString(R.string.unsubscribe) subscribed = true
}
} }
} }
} }
} }
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())
}
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()
}
} }
class SubscriptionChannelViewHolder(val binding: ChannelSubscriptionRowBinding) : class SubscriptionChannelViewHolder(val binding: ChannelSubscriptionRowBinding) :

View File

@ -1,96 +1,73 @@
package com.github.libretube.adapters package com.github.libretube.adapters
import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView 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.databinding.TrendingRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog 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.obj.StreamItem
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.formatShort 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( class TrendingAdapter(
private val videoFeed: List<StreamItem>, private val streamItems: List<StreamItem>,
private val childFragmentManager: FragmentManager private val childFragmentManager: FragmentManager,
) : RecyclerView.Adapter<TrendingViewHolder>() { private val showAllAtOne: Boolean = true
) : RecyclerView.Adapter<SubscriptionViewHolder>() {
private val TAG = "TrendingAdapter" private val TAG = "TrendingAdapter"
var index = 10
override fun getItemCount(): Int { 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 layoutInflater = LayoutInflater.from(parent.context)
val binding = TrendingRowBinding.inflate(layoutInflater, parent, false) val binding = TrendingRowBinding.inflate(layoutInflater, parent, false)
return TrendingViewHolder(binding) return SubscriptionViewHolder(binding)
} }
override fun onBindViewHolder(holder: TrendingViewHolder, position: Int) { override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
val trending = videoFeed[position] val trending = streamItems[position]
holder.binding.apply { holder.binding.apply {
textViewTitle.text = trending.title textViewTitle.text = trending.title
textViewChannel.text = textViewChannel.text =
trending.uploaderName + "" + trending.uploaderName + "" +
trending.views.formatShort() + "" + trending.views.formatShort() + "" +
DateUtils.getRelativeTimeSpanString(trending.uploaded!!) DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
if (trending.duration != -1L) { thumbnailDuration.setFormattedDuration(trending.duration!!)
thumbnailDuration.text = DateUtils.formatElapsedTime(trending.duration!!)
} else {
thumbnailDuration.text = root.context.getString(R.string.live)
thumbnailDuration.setBackgroundColor(R.attr.colorPrimaryDark)
}
channelImage.setOnClickListener { channelImage.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateChannel(root.context, trending.uploaderUrl)
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) {
}
} }
if (trending.thumbnail!!.isNotEmpty()) { ConnectionHelper.loadImage(trending.thumbnail, thumbnail)
Picasso.get().load(trending.thumbnail).into(thumbnail) ConnectionHelper.loadImage(trending.uploaderAvatar, channelImage)
}
if (trending.uploaderAvatar!!.isNotEmpty()) {
Picasso.get().load(trending.uploaderAvatar).into(channelImage)
}
root.setOnClickListener { root.setOnClickListener {
var bundle = Bundle() NavigationHelper.navigateVideo(root.context, trending.url)
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()
} }
val videoId = trending.url!!.toID()
root.setOnLongClickListener { root.setOnLongClickListener {
val videoId = trending.url!!.replace("/watch?v=", "") VideoOptionsDialog(videoId)
VideoOptionsDialog(videoId, root.context) .show(childFragmentManager, VideoOptionsDialog::class.java.name)
.show(childFragmentManager, VideoOptionsDialog.TAG)
true 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 package com.github.libretube.adapters
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView 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.databinding.WatchHistoryRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog 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.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( class WatchHistoryAdapter(
private val watchHistory: MutableList<WatchHistoryItem>, private val watchHistory: MutableList<WatchHistoryItem>,
@ -24,10 +20,10 @@ class WatchHistoryAdapter(
RecyclerView.Adapter<WatchHistoryViewHolder>() { RecyclerView.Adapter<WatchHistoryViewHolder>() {
private val TAG = "WatchHistoryAdapter" private val TAG = "WatchHistoryAdapter"
fun clear() { fun removeFromWatchHistory(position: Int) {
val size = watchHistory.size PreferenceHelper.removeFromWatchHistory(position)
watchHistory.clear() watchHistory.removeAt(position)
notifyItemRangeRemoved(0, size) notifyDataSetChanged()
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
@ -41,45 +37,29 @@ class WatchHistoryAdapter(
holder.binding.apply { holder.binding.apply {
videoTitle.text = video.title videoTitle.text = video.title
channelName.text = video.uploader channelName.text = video.uploader
uploadDate.text = video.uploadDate videoInfo.text = video.uploadDate
thumbnailDuration.text = DateUtils.formatElapsedTime(video.duration?.toLong()!!) thumbnailDuration.setFormattedDuration(video.duration!!)
Picasso.get().load(video.thumbnailUrl).into(thumbnail) ConnectionHelper.loadImage(video.thumbnailUrl, thumbnail)
Picasso.get().load(video.uploaderAvatar).into(channelImage) ConnectionHelper.loadImage(video.uploaderAvatar, channelImage)
channelImage.setOnClickListener { channelImage.setOnClickListener {
val activity = root.context as MainActivity NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
val bundle = bundleOf("channel_id" to video.uploaderUrl) }
activity.navController.navigate(R.id.channelFragment, bundle)
try { deleteBTN.setOnClickListener {
val mainMotionLayout = removeFromWatchHistory(position)
activity.findViewById<MotionLayout>(R.id.mainMotionLayout)
if (mainMotionLayout.progress == 0.toFloat()) {
mainMotionLayout.transitionToEnd()
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
.transitionToEnd()
}
} catch (e: Exception) {
}
} }
root.setOnClickListener { root.setOnClickListener {
var bundle = Bundle() NavigationHelper.navigateVideo(root.context, video.videoId)
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()
} }
root.setOnLongClickListener { root.setOnLongClickListener {
VideoOptionsDialog(video.videoId!!, root.context) VideoOptionsDialog(video.videoId!!)
.show(childFragmentManager, VideoOptionsDialog.TAG) .show(childFragmentManager, VideoOptionsDialog::class.java.name)
true 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.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.libretube.Globals
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.databinding.DialogAddtoplaylistBinding import com.github.libretube.databinding.DialogAddtoplaylistBinding
import com.github.libretube.obj.PlaylistId import com.github.libretube.obj.PlaylistId
@ -18,7 +19,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class AddtoPlaylistDialog : DialogFragment() { class AddToPlaylistDialog : DialogFragment() {
private val TAG = "AddToPlaylistDialog" private val TAG = "AddToPlaylistDialog"
private lateinit var binding: DialogAddtoplaylistBinding private lateinit var binding: DialogAddtoplaylistBinding
@ -32,7 +33,7 @@ class AddtoPlaylistDialog : DialogFragment() {
// Get the layout inflater // Get the layout inflater
binding = DialogAddtoplaylistBinding.inflate(layoutInflater) binding = DialogAddtoplaylistBinding.inflate(layoutInflater)
token = PreferenceHelper.getToken(requireContext()) token = PreferenceHelper.getToken()
if (token != "") fetchPlaylists() if (token != "") fetchPlaylists()
@ -59,24 +60,29 @@ class AddtoPlaylistDialog : DialogFragment() {
return@launchWhenCreated return@launchWhenCreated
} }
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
var names = emptyList<String>().toMutableList() val names = response.map { it.name }
for (playlist in response) {
names.add(playlist.name!!)
}
val arrayAdapter = val arrayAdapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names) ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names)
arrayAdapter.setDropDownViewResource( arrayAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item android.R.layout.simple_spinner_dropdown_item
) )
binding.playlistsSpinner.adapter = arrayAdapter 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 { runOnUiThread {
binding.addToPlaylist.setOnClickListener { binding.addToPlaylist.setOnClickListener {
val index = binding.playlistsSpinner.selectedItemPosition
Globals.SELECTED_PLAYLIST_ID = response[index].id!!
addToPlaylist( addToPlaylist(
response[binding.playlistsSpinner.selectedItemPosition].id!! response[index].id!!
) )
} }
} }
} else {
} }
} }
} }

View File

@ -33,7 +33,7 @@ class CreatePlaylistDialog : DialogFragment() {
dismiss() dismiss()
} }
token = PreferenceHelper.getToken(requireContext()) token = PreferenceHelper.getToken()
binding.createNewPlaylist.setOnClickListener { binding.createNewPlaylist.setOnClickListener {
// avoid creating the same playlist multiple times by spamming the button // 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.apiUrl).toURI()
URL(customInstance.frontendUrl).toURI() URL(customInstance.frontendUrl).toURI()
PreferenceHelper.saveCustomInstance(requireContext(), customInstance) PreferenceHelper.saveCustomInstance(customInstance)
activity?.recreate() activity?.recreate()
dismiss() dismiss()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -45,7 +45,7 @@ class DeleteAccountDialog : DialogFragment() {
private fun deleteAccount(password: String) { private fun deleteAccount(password: String) {
fun run() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val token = PreferenceHelper.getToken(requireContext()) val token = PreferenceHelper.getToken()
try { try {
RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password)) RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
@ -57,13 +57,13 @@ class DeleteAccountDialog : DialogFragment() {
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
logout() logout()
val restartDialog = RequireRestartDialog() val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog") restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
} }
} }
run() run()
} }
private fun logout() { private fun logout() {
PreferenceHelper.setToken(requireContext(), "") PreferenceHelper.setToken("")
} }
} }

View File

@ -1,24 +1,20 @@
package com.github.libretube.dialogs package com.github.libretube.dialogs
import android.Manifest
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.util.Log import android.util.Log
import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.view.size import androidx.core.view.size
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.databinding.DialogDownloadBinding import com.github.libretube.databinding.DialogDownloadBinding
import com.github.libretube.obj.Streams import com.github.libretube.obj.Streams
import com.github.libretube.services.DownloadService import com.github.libretube.services.DownloadService
import com.github.libretube.util.PermissionHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -36,48 +32,25 @@ class DownloadDialog : DialogFragment() {
return activity?.let { return activity?.let {
videoId = arguments?.getString("video_id")!! videoId = arguments?.getString("video_id")!!
val mainActivity = activity as MainActivity
val builder = MaterialAlertDialogBuilder(it) val builder = MaterialAlertDialogBuilder(it)
binding = DialogDownloadBinding.inflate(layoutInflater) binding = DialogDownloadBinding.inflate(layoutInflater)
fetchAvailableSources() fetchAvailableSources()
// request storage permissions if not granted yet PermissionHelper.requestReadWrite(requireActivity())
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
)
}
}
binding.title.text = ThemeHelper.getStyledAppName(requireContext()) 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.setView(binding.root)
builder.create() builder.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
@ -155,14 +128,15 @@ class DownloadDialog : DialogFragment() {
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1) if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
binding.download.setOnClickListener { binding.download.setOnClickListener {
val selectedAudioUrl = audioUrl[binding.audioSpinner.selectedItemPosition] val selectedAudioUrl =
val selectedVideoUrl = vidUrl[binding.videoSpinner.selectedItemPosition] 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) val intent = Intent(context, DownloadService::class.java)
intent.putExtra("videoId", videoId) intent.putExtra("videoName", streams.title)
intent.putExtra("videoUrl", selectedVideoUrl) intent.putExtra("videoUrl", selectedVideoUrl)
intent.putExtra("audioUrl", selectedAudioUrl) intent.putExtra("audioUrl", selectedAudioUrl)
intent.putExtra("duration", duration)
context?.startService(intent) context?.startService(intent)
dismiss() 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() Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show()
} else if (response.token != null) { } else if (response.token != null) {
Toast.makeText(context, R.string.loggedIn, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.loggedIn, Toast.LENGTH_SHORT).show()
PreferenceHelper.setToken(requireContext(), response.token!!) PreferenceHelper.setToken(response.token!!)
PreferenceHelper.setUsername(requireContext(), login.username!!) PreferenceHelper.setUsername(login.username!!)
val restartDialog = RequireRestartDialog()
restartDialog.show(parentFragmentManager, "RequireRestartDialog")
dialog?.dismiss() dialog?.dismiss()
activity?.recreate()
} }
} }
} }
@ -112,8 +111,8 @@ class LoginDialog : DialogFragment() {
Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show() Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show()
} else if (response.token != null) { } else if (response.token != null) {
Toast.makeText(context, R.string.registered, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.registered, Toast.LENGTH_SHORT).show()
PreferenceHelper.setToken(requireContext(), response.token!!) PreferenceHelper.setToken(response.token!!)
PreferenceHelper.setUsername(requireContext(), login.username!!) PreferenceHelper.setUsername(login.username!!)
dialog?.dismiss() dialog?.dismiss()
} }
} }

View File

@ -19,13 +19,13 @@ class LogoutDialog : DialogFragment() {
val builder = MaterialAlertDialogBuilder(it) val builder = MaterialAlertDialogBuilder(it)
binding = DialogLogoutBinding.inflate(layoutInflater) binding = DialogLogoutBinding.inflate(layoutInflater)
val user = PreferenceHelper.getUsername(requireContext()) val user = PreferenceHelper.getUsername()
binding.user.text = binding.user.text =
binding.user.text.toString() + " (" + user + ")" binding.user.text.toString() + " (" + user + ")"
binding.logout.setOnClickListener { binding.logout.setOnClickListener {
Toast.makeText(context, R.string.loggedout, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.loggedout, Toast.LENGTH_SHORT).show()
PreferenceHelper.setToken(requireContext(), "") PreferenceHelper.setToken("")
dialog?.dismiss() dialog?.dismiss()
activity?.recreate() activity?.recreate()
} }

View File

@ -1,7 +1,6 @@
package com.github.libretube.dialogs package com.github.libretube.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
@ -10,27 +9,31 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.obj.PlaylistId import com.github.libretube.obj.PlaylistId
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.toID
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class PlaylistOptionsDialog( class PlaylistOptionsDialog(
private val playlistId: String, private val playlistId: String,
private val isOwner: Boolean, private val isOwner: Boolean
context: Context
) : DialogFragment() { ) : DialogFragment() {
val TAG = "PlaylistOptionsDialog" val TAG = "PlaylistOptionsDialog"
private var optionsList = listOf(
context.getString(R.string.clonePlaylist),
context.getString(R.string.share)
)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// options for the dialog
var optionsList = listOf(
context?.getString(R.string.playOnBackground),
context?.getString(R.string.clonePlaylist),
context?.getString(R.string.share)
)
if (isOwner) { if (isOwner) {
optionsList = optionsList + optionsList = optionsList +
context?.getString(R.string.deletePlaylist)!! - context?.getString(R.string.deletePlaylist)!! -
@ -49,9 +52,22 @@ class PlaylistOptionsDialog(
) )
) { _, which -> ) { _, which ->
when (optionsList[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 // Clone the playlist to the users Piped account
context?.getString(R.string.clonePlaylist) -> { context?.getString(R.string.clonePlaylist) -> {
val token = PreferenceHelper.getToken(requireContext()) val token = PreferenceHelper.getToken()
if (token != "") { if (token != "") {
importPlaylist(token, playlistId) importPlaylist(token, playlistId)
} else { } else {
@ -66,10 +82,10 @@ class PlaylistOptionsDialog(
context?.getString(R.string.share) -> { context?.getString(R.string.share) -> {
val shareDialog = ShareDialog(playlistId, true) val shareDialog = ShareDialog(playlistId, true)
// using parentFragmentManager, childFragmentManager doesn't work here // using parentFragmentManager, childFragmentManager doesn't work here
shareDialog.show(parentFragmentManager, "ShareDialog") shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
} }
context?.getString(R.string.deletePlaylist) -> { context?.getString(R.string.deletePlaylist) -> {
val token = PreferenceHelper.getToken(requireContext()) val token = PreferenceHelper.getToken()
deletePlaylist(playlistId, token) deletePlaylist(playlistId, token)
} }
} }

View File

@ -18,7 +18,7 @@ class RequireRestartDialog : DialogFragment() {
activity?.recreate() activity?.recreate()
ThemeHelper.restartMainActivity(requireContext()) ThemeHelper.restartMainActivity(requireContext())
} }
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(R.string.cancel, null)
.create() .create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: 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.R
import com.github.libretube.YOUTUBE_FRONTEND_URL import com.github.libretube.YOUTUBE_FRONTEND_URL
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ShareDialog( class ShareDialog(
private val id: String, private val id: String,
private val isPlaylist: Boolean private val isPlaylist: Boolean,
private val position: Long = 0L
) : DialogFragment() { ) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -38,7 +40,14 @@ class ShareDialog(
else -> instanceUrl else -> instanceUrl
} }
val path = if (!isPlaylist) "/watch?v=$id" else "/playlist?list=$id" 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() val intent = Intent()
intent.apply { intent.apply {
@ -57,13 +66,12 @@ class ShareDialog(
// get the frontend url if it's a custom instance // get the frontend url if it's a custom instance
private fun getCustomInstanceFrontendUrl(): String { private fun getCustomInstanceFrontendUrl(): String {
val instancePref = PreferenceHelper.getString( val instancePref = PreferenceHelper.getString(
requireContext(), PreferenceKeys.FETCH_INSTANCE,
"selectInstance",
PIPED_FRONTEND_URL PIPED_FRONTEND_URL
) )
// get the api urls of the other custom instances // 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 // return the custom instance frontend url if available
customInstances.forEach { instance -> customInstances.forEach { instance ->

View File

@ -3,43 +3,51 @@ package com.github.libretube.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.github.libretube.R 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 import com.google.android.material.dialog.MaterialAlertDialogBuilder
class UpdateAvailableDialog( class UpdateDialog(
private val versionTag: String, private val updateInfo: UpdateInfo
private val updateLink: String
) : DialogFragment() { ) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { return activity?.let {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(context?.getString(R.string.update_available, versionTag)) .setTitle(context?.getString(R.string.update_available, updateInfo.name))
.setMessage(context?.getString(R.string.update_available_text)) .setMessage(context?.getString(R.string.update_now))
.setNegativeButton(context?.getString(R.string.cancel)) { _, _ -> .setNegativeButton(R.string.cancel, null)
dismiss()
}
.setPositiveButton(context?.getString(R.string.okay)) { _, _ -> .setPositiveButton(context?.getString(R.string.okay)) { _, _ ->
val uri = Uri.parse(updateLink) val downloadUrl = getDownloadUrl(updateInfo)
val intent = Intent(Intent.ACTION_VIEW).setData(uri) Log.i("downloadUrl", downloadUrl.toString())
startActivity(intent) 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() .create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
}
class NoUpdateAvailableDialog() : DialogFragment() { private fun getDownloadUrl(updateInfo: UpdateInfo): String? {
val supportedArchitectures = Build.SUPPORTED_ABIS
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { supportedArchitectures.forEach { arch ->
return activity?.let { updateInfo.assets?.forEach { asset ->
MaterialAlertDialogBuilder(requireContext()) if (asset.name?.contains(arch) == true) return asset.browser_download_url
.setTitle(context?.getString(R.string.app_uptodate)) }
.setMessage(context?.getString(R.string.no_update_available)) }
.setPositiveButton(context?.getString(R.string.okay)) { _, _ -> } return null
.create()
} ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View File

@ -1,14 +1,17 @@
package com.github.libretube.dialogs package com.github.libretube.dialogs
import android.app.Dialog import android.app.Dialog
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment 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.R
import com.github.libretube.preferences.PreferenceHelper 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 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. * Needs the [videoId] to load the content from the right video.
*/ */
class VideoOptionsDialog(private val videoId: String, context: Context) : DialogFragment() { class VideoOptionsDialog(
/** private val videoId: String
* List that stores the different menu options. In the future could be add more options here. ) : DialogFragment() {
*/ private val TAG = "VideoOptionsDialog"
private val optionsList = listOf(
context.getString(R.string.playOnBackground),
context.getString(R.string.addToPlaylist),
context.getString(R.string.share)
)
/** /**
* Dialog that returns a [MaterialAlertDialogBuilder] showing a menu of options. * Dialog that returns a [MaterialAlertDialogBuilder] showing a menu of options.
*/ */
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext()) /**
.setNegativeButton(R.string.cancel) { dialog, _ -> * List that stores the different menu options. In the future could be add more options here.
dialog.dismiss() */
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( .setAdapter(
ArrayAdapter( ArrayAdapter(
requireContext(), requireContext(),
@ -41,23 +56,23 @@ class VideoOptionsDialog(private val videoId: String, context: Context) : Dialog
optionsList optionsList
) )
) { _, which -> ) { _, 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]) { when (optionsList[which]) {
// This for example will be the "Background mode" option // Start the background mode
context?.getString(R.string.playOnBackground) -> { context?.getString(R.string.playOnBackground) -> {
BackgroundMode.getInstance() BackgroundHelper.playOnBackground(requireContext(), videoId)
.playOnBackgroundMode(requireContext(), videoId)
} }
// Add Video to Playlist Dialog // Add Video to Playlist Dialog
context?.getString(R.string.addToPlaylist) -> { context?.getString(R.string.addToPlaylist) -> {
val token = PreferenceHelper.getToken(requireContext()) val token = PreferenceHelper.getToken()
if (token != "") { if (token != "") {
val newFragment = AddtoPlaylistDialog() val newFragment = AddToPlaylistDialog()
val bundle = Bundle() val bundle = Bundle()
bundle.putString("videoId", videoId) bundle.putString("videoId", videoId)
newFragment.arguments = bundle newFragment.arguments = bundle
newFragment.show(parentFragmentManager, "AddToPlaylist") newFragment.show(
parentFragmentManager,
AddToPlaylistDialog::class.java.name
)
} else { } else {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show() 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) -> { context?.getString(R.string.share) -> {
val shareDialog = ShareDialog(videoId, false) val shareDialog = ShareDialog(videoId, false)
// using parentFragmentManager is important here // 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() .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 package com.github.libretube.fragments
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.adapters.ChannelAdapter import com.github.libretube.adapters.ChannelAdapter
import com.github.libretube.databinding.FragmentChannelBinding import com.github.libretube.databinding.FragmentChannelBinding
import com.github.libretube.obj.Subscribe import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.SubscriptionHelper
import com.github.libretube.util.formatShort import com.github.libretube.util.formatShort
import com.squareup.picasso.Picasso import com.github.libretube.util.toID
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class ChannelFragment : Fragment() { class ChannelFragment : BaseFragment() {
private val TAG = "ChannelFragment" private val TAG = "ChannelFragment"
private lateinit var binding: FragmentChannelBinding private lateinit var binding: FragmentChannelBinding
private var channelId: String? = null private var channelId: String? = null
private var channelName: String? = null
var nextPage: String? = null var nextPage: String? = null
private var channelAdapter: ChannelAdapter? = null private var channelAdapter: ChannelAdapter? = null
private var isLoading = true private var isLoading = true
private var isSubscribed: Boolean = false private var isSubscribed: Boolean? = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { arguments?.let {
channelId = it.getString("channel_id") channelId = it.getString("channel_id").toID()
channelName = it.getString("channel_name")
?.replace("/c/", "")
?.replace("/user/", "")
} }
} }
@ -49,16 +53,13 @@ class ChannelFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
channelId = channelId!!.replace("/channel/", "")
binding.channelName.text = channelId binding.channelName.text = channelId
binding.channelRecView.layoutManager = LinearLayoutManager(context) binding.channelRecView.layoutManager = LinearLayoutManager(context)
val refreshChannel = { val refreshChannel = {
binding.channelRefresh.isRefreshing = true binding.channelRefresh.isRefreshing = true
fetchChannel() fetchChannel()
if (PreferenceHelper.getToken(requireContext()) != "") { isSubscribed()
isSubscribed()
}
} }
refreshChannel() refreshChannel()
binding.channelRefresh.setOnRefreshListener { binding.channelRefresh.setOnRefreshListener {
@ -74,92 +75,43 @@ class ChannelFragment : Fragment() {
if (nextPage != null && !isLoading) { if (nextPage != null && !isLoading) {
isLoading = true isLoading = true
binding.channelRefresh.isRefreshing = true binding.channelRefresh.isRefreshing = true
fetchNextPage() fetchChannelNextPage()
} }
} }
} }
} }
private fun isSubscribed() { private fun isSubscribed() {
@SuppressLint("ResourceAsColor") lifecycleScope.launchWhenCreated {
fun run() { isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
lifecycleScope.launchWhenCreated { if (isSubscribed == null) return@launchWhenCreated
val response = try {
val token = PreferenceHelper.getToken(requireContext()) runOnUiThread {
RetrofitInstance.authApi.isSubscribed( if (isSubscribed == true) {
channelId!!, binding.channelSubscribe.text = getString(R.string.unsubscribe)
token
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
return@launchWhenCreated
} }
runOnUiThread { binding.channelSubscribe.setOnClickListener {
if (response.subscribed == true) { binding.channelSubscribe.text = if (isSubscribed == true) {
SubscriptionHelper.unsubscribe(channelId!!)
isSubscribed = false
getString(R.string.subscribe)
} else {
SubscriptionHelper.subscribe(channelId!!)
isSubscribed = true isSubscribed = true
binding.channelSubscribe.text = getString(R.string.unsubscribe) getString(R.string.unsubscribe)
}
if (response.subscribed != null) {
binding.channelSubscribe.apply {
setOnClickListener {
text = if (isSubscribed) {
unsubscribe()
getString(R.string.subscribe)
} else {
subscribe()
getString(R.string.unsubscribe)
}
}
}
} }
} }
} }
} }
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() { private fun fetchChannel() {
fun run() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { val response = try {
RetrofitInstance.api.getChannel(channelId!!) if (channelId != null) RetrofitInstance.api.getChannel(channelId!!)
else RetrofitInstance.api.getChannelByName(channelName!!)
} catch (e: IOException) { } catch (e: IOException) {
binding.channelRefresh.isRefreshing = false binding.channelRefresh.isRefreshing = false
println(e) println(e)
@ -194,8 +146,10 @@ class ChannelFragment : Fragment() {
binding.channelDescription.text = response.description?.trim() binding.channelDescription.text = response.description?.trim()
} }
Picasso.get().load(response.bannerUrl).into(binding.channelBanner) ConnectionHelper.loadImage(response.bannerUrl, binding.channelBanner)
Picasso.get().load(response.avatarUrl).into(binding.channelImage) ConnectionHelper.loadImage(response.avatarUrl, binding.channelImage)
// recyclerview of the videos by the channel
channelAdapter = ChannelAdapter( channelAdapter = ChannelAdapter(
response.relatedStreams!!.toMutableList(), response.relatedStreams!!.toMutableList(),
childFragmentManager childFragmentManager
@ -207,7 +161,7 @@ class ChannelFragment : Fragment() {
run() run()
} }
private fun fetchNextPage() { private fun fetchChannelNextPage() {
fun run() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { val response = try {
@ -230,10 +184,4 @@ class ChannelFragment : Fragment() {
} }
run() 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.adapters.TrendingAdapter import com.github.libretube.adapters.TrendingAdapter
import com.github.libretube.databinding.FragmentHomeBinding import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.LocaleHelper import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class HomeFragment : Fragment() { class HomeFragment : BaseFragment() {
private val TAG = "HomeFragment" private val TAG = "HomeFragment"
private lateinit var binding: FragmentHomeBinding private lateinit var binding: FragmentHomeBinding
private lateinit var region: String private lateinit var region: String
@ -41,12 +42,11 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val grid = PreferenceHelper.getString( val grid = PreferenceHelper.getString(
requireContext(), PreferenceKeys.GRID_COLUMNS,
"grid",
resources.getInteger(R.integer.grid_items).toString() 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 // get the system default country if auto region selected
region = if (regionPref == "sys") { region = if (regionPref == "sys") {
@ -88,10 +88,4 @@ class HomeFragment : Fragment() {
} }
run() 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.Globals import com.github.libretube.Globals
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.adapters.PlaylistsAdapter import com.github.libretube.adapters.PlaylistsAdapter
import com.github.libretube.databinding.FragmentLibraryBinding import com.github.libretube.databinding.FragmentLibraryBinding
import com.github.libretube.dialogs.CreatePlaylistDialog import com.github.libretube.dialogs.CreatePlaylistDialog
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class LibraryFragment : Fragment() { class LibraryFragment : BaseFragment() {
private val TAG = "LibraryFragment" private val TAG = "LibraryFragment"
lateinit var token: String lateinit var token: String
@ -43,12 +45,12 @@ class LibraryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.playlistRecView.layoutManager = LinearLayoutManager(view.context) binding.playlistRecView.layoutManager = LinearLayoutManager(requireContext())
token = PreferenceHelper.getToken(requireContext()) token = PreferenceHelper.getToken()
// hide watch history button of history disabled // hide watch history button of history disabled
val watchHistoryEnabled = val watchHistoryEnabled =
PreferenceHelper.getBoolean(requireContext(), "watch_history_toggle", true) PreferenceHelper.getBoolean(PreferenceKeys.WATCH_HISTORY_TOGGLE, true)
if (!watchHistoryEnabled) { if (!watchHistoryEnabled) {
binding.showWatchHistory.visibility = View.GONE binding.showWatchHistory.visibility = View.GONE
} else { } else {
@ -58,8 +60,7 @@ class LibraryFragment : Fragment() {
} }
if (token != "") { if (token != "") {
binding.boogh.visibility = View.GONE binding.loginOrRegister.visibility = View.GONE
binding.textLike.visibility = View.GONE
fetchPlaylists() fetchPlaylists()
binding.playlistRefresh.isEnabled = true binding.playlistRefresh.isEnabled = true
binding.playlistRefresh.setOnRefreshListener { binding.playlistRefresh.setOnRefreshListener {
@ -67,7 +68,7 @@ class LibraryFragment : Fragment() {
} }
binding.createPlaylist.setOnClickListener { binding.createPlaylist.setOnClickListener {
val newFragment = CreatePlaylistDialog() val newFragment = CreatePlaylistDialog()
newFragment.show(childFragmentManager, "Create Playlist") newFragment.show(childFragmentManager, CreatePlaylistDialog::class.java.name)
} }
} else { } else {
binding.playlistRefresh.isEnabled = false binding.playlistRefresh.isEnabled = false
@ -78,7 +79,7 @@ class LibraryFragment : Fragment() {
override fun onResume() { override fun onResume() {
// optimize CreatePlaylistFab bottom margin if miniPlayer active // optimize CreatePlaylistFab bottom margin if miniPlayer active
val layoutParams = binding.createPlaylist.layoutParams as ViewGroup.MarginLayoutParams 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 binding.createPlaylist.layoutParams = layoutParams
super.onResume() super.onResume()
} }
@ -102,35 +103,36 @@ class LibraryFragment : Fragment() {
binding.playlistRefresh.isRefreshing = false binding.playlistRefresh.isRefreshing = false
} }
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
runOnUiThread { binding.loginOrRegister.visibility = View.GONE
binding.boogh.visibility = View.GONE
binding.textLike.visibility = View.GONE
}
val playlistsAdapter = PlaylistsAdapter( val playlistsAdapter = PlaylistsAdapter(
response.toMutableList(), response.toMutableList(),
childFragmentManager,
requireActivity() 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 binding.playlistRecView.adapter = playlistsAdapter
} else { } else {
runOnUiThread { runOnUiThread {
binding.boogh.apply { binding.loginOrRegister.visibility = View.VISIBLE
visibility = View.VISIBLE binding.boogh.setImageResource(R.drawable.ic_list)
setImageResource(R.drawable.ic_list) binding.textLike.text = getString(R.string.emptyList)
}
binding.textLike.apply {
visibility = View.VISIBLE
text = getString(R.string.emptyList)
}
} }
} }
} }
} }
run() 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.adapters.PlaylistAdapter import com.github.libretube.adapters.PlaylistAdapter
import com.github.libretube.databinding.FragmentPlaylistBinding import com.github.libretube.databinding.FragmentPlaylistBinding
import com.github.libretube.dialogs.PlaylistOptionsDialog 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.RetrofitInstance
import com.github.libretube.util.toID
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class PlaylistFragment : Fragment() { class PlaylistFragment : BaseFragment() {
private val TAG = "PlaylistFragment" private val TAG = "PlaylistFragment"
private lateinit var binding: FragmentPlaylistBinding private lateinit var binding: FragmentPlaylistBinding
private var playlistId: String? = null 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 playlistAdapter: PlaylistAdapter? = null
private var isLoading = true private var isLoading = true
@ -30,6 +33,7 @@ class PlaylistFragment : Fragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { arguments?.let {
playlistId = it.getString("playlist_id") playlistId = it.getString("playlist_id")
isOwner = it.getBoolean("isOwner")
} }
} }
@ -45,7 +49,7 @@ class PlaylistFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
playlistId = playlistId!!.replace("/playlist?list=", "") playlistId = playlistId!!.toID()
binding.playlistRecView.layoutManager = LinearLayoutManager(context) binding.playlistRecView.layoutManager = LinearLayoutManager(context)
binding.playlistProgress.visibility = View.VISIBLE binding.playlistProgress.visibility = View.VISIBLE
@ -56,7 +60,9 @@ class PlaylistFragment : Fragment() {
fun run() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { 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) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG, "IOException, you might not have internet connection") Log.e(TAG, "IOException, you might not have internet connection")
@ -70,20 +76,18 @@ class PlaylistFragment : Fragment() {
runOnUiThread { runOnUiThread {
binding.playlistProgress.visibility = View.GONE binding.playlistProgress.visibility = View.GONE
binding.playlistName.text = response.name binding.playlistName.text = response.name
binding.playlistUploader.text = response.uploader binding.uploader.text = response.uploader
binding.playlistTotVideos.text = binding.videoCount.text =
getString(R.string.videoCount, response.videos.toString()) 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 // show playlist options
binding.optionsMenu.setOnClickListener { binding.optionsMenu.setOnClickListener {
val optionsDialog = val optionsDialog =
PlaylistOptionsDialog(playlistId!!, isOwner, requireContext()) PlaylistOptionsDialog(playlistId!!, isOwner)
optionsDialog.show(childFragmentManager, "PlaylistOptionsDialog") optionsDialog.show(
childFragmentManager,
PlaylistOptionsDialog::class.java.name
)
} }
playlistAdapter = PlaylistAdapter( playlistAdapter = PlaylistAdapter(
@ -93,6 +97,16 @@ class PlaylistFragment : Fragment() {
requireActivity(), requireActivity(),
childFragmentManager 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.playlistRecView.adapter = playlistAdapter
binding.playlistScrollview.viewTreeObserver binding.playlistScrollview.viewTreeObserver
.addOnScrollChangedListener { .addOnScrollChangedListener {
@ -104,10 +118,37 @@ class PlaylistFragment : Fragment() {
isLoading = true isLoading = true
fetchNextPage() 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() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { 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) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG, "IOException, you might not have internet connection") Log.e(TAG, "IOException, you might not have internet connection")
@ -134,10 +182,4 @@ class PlaylistFragment : Fragment() {
} }
run() 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 package com.github.libretube.fragments
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN import androidx.fragment.app.activityViewModels
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.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.activities.MainActivity
import com.github.libretube.activities.hideKeyboard
import com.github.libretube.adapters.SearchAdapter
import com.github.libretube.adapters.SearchHistoryAdapter import com.github.libretube.adapters.SearchHistoryAdapter
import com.github.libretube.adapters.SearchSuggestionsAdapter import com.github.libretube.adapters.SearchSuggestionsAdapter
import com.github.libretube.databinding.FragmentSearchBinding 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.preferences.PreferenceHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class SearchFragment : Fragment() { class SearchFragment() : BaseFragment() {
private val TAG = "SearchFragment" private val TAG = "SearchFragment"
private lateinit var binding: FragmentSearchBinding private lateinit var binding: FragmentSearchBinding
private val viewModel: SearchViewModel by activityViewModels()
private var selectedFilter = 0 private var query: String? = null
private var apiSearchFilter = "all"
private var nextPage: String? = null
private var searchAdapter: SearchAdapter? = null
private var isLoading: Boolean = true
private var isFetchingSearch: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { query = arguments?.getString("query")
}
} }
override fun onCreateView( override fun onCreateView(
@ -61,109 +43,25 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
var tempSelectedItem = 0 binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.clearSearchImageView.setOnClickListener { // waiting for the query to change
binding.autoCompleteTextView.text.clear() viewModel.searchQuery.observe(viewLifecycleOwner) {
showData(it)
} }
binding.filterMenuImageView.setOnClickListener {
val filterOptions = arrayOf(
getString(R.string.all),
getString(R.string.videos),
getString(R.string.channels),
getString(R.string.playlists),
getString(R.string.music_songs),
getString(R.string.music_videos),
getString(R.string.music_albums),
getString(R.string.music_playlists)
)
MaterialAlertDialogBuilder(view.context)
.setTitle(getString(R.string.choose_filter))
.setSingleChoiceItems(filterOptions, selectedFilter) { _, id ->
tempSelectedItem = id
}
.setPositiveButton(
getString(R.string.okay)
) { _, _ ->
selectedFilter = tempSelectedItem
apiSearchFilter = when (selectedFilter) {
0 -> "all"
1 -> "videos"
2 -> "channels"
3 -> "playlists"
4 -> "music_songs"
5 -> "music_videos"
6 -> "music_albums"
7 -> "music_playlists"
else -> "all"
}
fetchSearch(binding.autoCompleteTextView.text.toString())
}
.setNegativeButton(getString(R.string.cancel), null)
.create()
.show()
}
// show search history
binding.historyRecycler.layoutManager = LinearLayoutManager(view.context)
showHistory()
binding.searchRecycler.layoutManager = GridLayoutManager(view.context, 1)
binding.autoCompleteTextView.requestFocus()
val imm =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.autoCompleteTextView, InputMethodManager.SHOW_IMPLICIT)
binding.autoCompleteTextView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s!! != "") {
binding.searchRecycler.adapter = null
binding.searchRecycler.viewTreeObserver
.addOnScrollChangedListener {
if (!binding.searchRecycler.canScrollVertically(1)) {
fetchNextSearchItems(binding.autoCompleteTextView.text.toString())
}
}
fetchSuggestions(s.toString(), binding.autoCompleteTextView)
}
}
override fun afterTextChanged(s: Editable?) {
if (s!!.isEmpty()) {
showHistory()
}
}
})
binding.autoCompleteTextView.setOnEditorActionListener(
OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
hideKeyboard()
binding.searchRecycler.visibility = VISIBLE
binding.historyRecycler.visibility = GONE
fetchSearch(binding.autoCompleteTextView.text.toString())
return@OnEditorActionListener true
}
false
}
)
} }
private fun fetchSuggestions(query: String, autoTextView: EditText) { private fun showData(query: String?) {
// fetch the search or history
binding.historyEmpty.visibility = View.GONE
binding.suggestionsRecycler.visibility = View.VISIBLE
if (query == null || query == "") showHistory()
else fetchSuggestions(query)
}
private fun fetchSuggestions(query: String) {
fun run() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
binding.searchRecycler.visibility = GONE
binding.historyRecycler.visibility = VISIBLE
val response = try { val response = try {
RetrofitInstance.api.getSuggestions(query) RetrofitInstance.api.getSuggestions(query)
} catch (e: IOException) { } catch (e: IOException) {
@ -174,118 +72,33 @@ class SearchFragment : Fragment() {
Log.e(TAG, "HttpException, unexpected response") Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated return@launchWhenCreated
} }
// only load the suggestions if the input field didn't get cleared yet
val suggestionsAdapter = val suggestionsAdapter =
SearchSuggestionsAdapter(response, autoTextView, this@SearchFragment) SearchSuggestionsAdapter(
binding.historyRecycler.adapter = suggestionsAdapter response,
} (activity as MainActivity).searchView
}
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!!
) )
} catch (e: IOException) { runOnUiThread {
println(e) if (viewModel.searchQuery.value != "") {
Log.e(TAG, "IOException, you might not have internet connection") binding.suggestionsRecycler.adapter = suggestionsAdapter
return@launchWhenCreated }
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response," + e.response())
return@launchWhenCreated
} }
nextPage = response.nextpage
searchAdapter?.updateItems(response.items!!)
isLoading = false
} }
} }
} run()
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()
} }
private fun showHistory() { private fun showHistory() {
binding.searchRecycler.visibility = GONE val historyList = PreferenceHelper.getSearchHistory()
val historyList = PreferenceHelper.getHistory(requireContext())
if (historyList.isNotEmpty()) { if (historyList.isNotEmpty()) {
binding.historyRecycler.adapter = binding.suggestionsRecycler.adapter =
SearchHistoryAdapter( SearchHistoryAdapter(
requireContext(),
historyList, historyList,
binding.autoCompleteTextView, (activity as MainActivity).searchView
this
) )
binding.historyRecycler.visibility = VISIBLE } else {
} binding.suggestionsRecycler.visibility = View.GONE
} binding.historyEmpty.visibility = View.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)
} }
} }
} }

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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.adapters.SubscriptionAdapter
import com.github.libretube.adapters.SubscriptionChannelAdapter import com.github.libretube.adapters.SubscriptionChannelAdapter
import com.github.libretube.adapters.TrendingAdapter
import com.github.libretube.databinding.FragmentSubscriptionsBinding 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.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance 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 retrofit2.HttpException
import java.io.IOException import java.io.IOException
class SubscriptionsFragment : Fragment() { class SubscriptionsFragment : BaseFragment() {
val TAG = "SubFragment" val TAG = "SubFragment"
private lateinit var binding: FragmentSubscriptionsBinding private lateinit var binding: FragmentSubscriptionsBinding
lateinit var token: String lateinit var token: String
private var isLoaded = false 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -47,69 +52,83 @@ class SubscriptionsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
token = PreferenceHelper.getToken(requireContext()) token = PreferenceHelper.getToken()
if (token != "") { binding.subRefresh.isEnabled = true
binding.loginOrRegister.visibility = View.GONE
binding.subRefresh.isEnabled = true
binding.subProgress.visibility = View.VISIBLE binding.subProgress.visibility = View.VISIBLE
val grid = PreferenceHelper.getString( val grid = PreferenceHelper.getString(
requireContext(), PreferenceKeys.GRID_COLUMNS,
"grid", resources.getInteger(R.integer.grid_items).toString()
resources.getInteger(R.integer.grid_items).toString() )
)!! binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt()) fetchFeed()
fetchFeed(binding.subFeed, binding.subProgress)
binding.subRefresh.setOnRefreshListener { binding.subRefresh.setOnRefreshListener {
fetchChannels(binding.subChannels) fetchChannels()
fetchFeed(binding.subFeed, binding.subProgress) fetchFeed()
}
binding.toggleSubs.visibility = View.VISIBLE
var loadedSubbedChannels = false
binding.toggleSubs.setOnClickListener {
binding.toggle.animate().rotationBy(180F).setDuration(100).start()
if (!binding.subChannels.isVisible) {
if (!loadedSubbedChannels) {
binding.subChannels.layoutManager = LinearLayoutManager(context)
fetchChannels(binding.subChannels)
loadedSubbedChannels = true
}
binding.subChannels.visibility = View.VISIBLE
binding.subFeed.visibility = View.GONE
} else {
binding.subChannels.visibility = View.GONE
binding.subFeed.visibility = View.VISIBLE
}
}
binding.scrollviewSub.viewTreeObserver
.addOnScrollChangedListener {
if (binding.scrollviewSub.getChildAt(0).bottom
== (binding.scrollviewSub.height + binding.scrollviewSub.scrollY)
) {
// scroll view is at bottom
if (isLoaded) {
binding.subRefresh.isRefreshing = true
subscriptionAdapter?.updateItems()
binding.subRefresh.isRefreshing = false
}
}
}
} else {
binding.subRefresh.isEnabled = false
} }
binding.sortTV.setOnClickListener {
showSortDialog()
}
binding.toggleSubs.visibility = View.VISIBLE
var loadedSubbedChannels = false
binding.toggleSubs.setOnClickListener {
if (!binding.subChannelsContainer.isVisible) {
if (!loadedSubbedChannels) {
binding.subChannels.layoutManager = LinearLayoutManager(context)
fetchChannels()
loadedSubbedChannels = true
}
binding.subChannelsContainer.visibility = View.VISIBLE
binding.subFeedContainer.visibility = View.GONE
} else {
binding.subChannelsContainer.visibility = View.GONE
binding.subFeedContainer.visibility = View.VISIBLE
}
}
binding.scrollviewSub.viewTreeObserver
.addOnScrollChangedListener {
if (binding.scrollviewSub.getChildAt(0).bottom
== (binding.scrollviewSub.height + binding.scrollviewSub.scrollY)
) {
// scroll view is at bottom
if (isLoaded) {
binding.subRefresh.isRefreshing = true
subscriptionAdapter?.updateItems()
binding.subRefresh.isRefreshing = false
}
}
}
} }
private fun fetchFeed(feedRecView: RecyclerView, progressBar: ProgressBar) { private fun showSortDialog() {
val sortOptions = resources.getStringArray(R.array.sortOptions)
val sortOptionValues = resources.getStringArray(R.array.sortOptionsValues)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.sort)
.setItems(sortOptions) { _, index ->
binding.sortTV.text = sortOptions[index]
sortOrder = sortOptionValues[index]
showFeed()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun fetchFeed() {
fun run() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { feed = try {
RetrofitInstance.authApi.getFeed(token) if (token != "") RetrofitInstance.authApi.getFeed(token)
else RetrofitInstance.authApi.getUnauthenticatedFeed(
SubscriptionHelper.getFormattedLocalSubscriptions()
)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
Log.e(TAG, "IOException, you might not have internet connection") Log.e(TAG, "IOException, you might not have internet connection")
@ -120,35 +139,46 @@ class SubscriptionsFragment : Fragment() {
} finally { } finally {
binding.subRefresh.isRefreshing = false binding.subRefresh.isRefreshing = false
} }
if (response.isNotEmpty()) { if (feed.isNotEmpty()) {
subscriptionAdapter = SubscriptionAdapter(response, childFragmentManager) // save the last recent video to the prefs for the notification worker
feedRecView.adapter = subscriptionAdapter PreferenceHelper.setLatestVideoId(feed[0].url.toID())
subscriptionAdapter?.updateItems() // show the feed
showFeed()
} else { } else {
runOnUiThread { runOnUiThread {
with(binding.boogh) { binding.emptyFeed.visibility = View.VISIBLE
visibility = View.VISIBLE
setImageResource(R.drawable.ic_list)
}
with(binding.textLike) {
visibility = View.VISIBLE
text = getString(R.string.emptyList)
}
binding.loginOrRegister.visibility = View.VISIBLE
} }
} }
progressBar.visibility = View.GONE binding.subProgress.visibility = View.GONE
isLoaded = true isLoaded = true
} }
} }
run() 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() { fun run() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { val response = try {
RetrofitInstance.authApi.subscriptions(token) if (token != "") RetrofitInstance.authApi.subscriptions(token)
else RetrofitInstance.authApi.unauthenticatedSubscriptions(
SubscriptionHelper.getFormattedLocalSubscriptions()
)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
Log.e(TAG, "IOException, you might not have internet connection") Log.e(TAG, "IOException, you might not have internet connection")
@ -160,7 +190,8 @@ class SubscriptionsFragment : Fragment() {
binding.subRefresh.isRefreshing = false binding.subRefresh.isRefreshing = false
} }
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
channelRecView.adapter = SubscriptionChannelAdapter(response.toMutableList()) binding.subChannels.adapter =
SubscriptionChannelAdapter(response.toMutableList())
} else { } else {
Toast.makeText(context, R.string.subscribeIsEmpty, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.subscribeIsEmpty, Toast.LENGTH_SHORT).show()
} }
@ -168,10 +199,4 @@ class SubscriptionsFragment : Fragment() {
} }
run() 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.adapters.WatchHistoryAdapter import com.github.libretube.adapters.WatchHistoryAdapter
import com.github.libretube.databinding.FragmentWatchHistoryBinding import com.github.libretube.databinding.FragmentWatchHistoryBinding
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
class WatchHistoryFragment : Fragment() { class WatchHistoryFragment : BaseFragment() {
private val TAG = "WatchHistoryFragment" private val TAG = "WatchHistoryFragment"
private lateinit var binding: FragmentWatchHistoryBinding private lateinit var binding: FragmentWatchHistoryBinding
@ -26,20 +28,58 @@ class WatchHistoryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val watchHistory = PreferenceHelper.getWatchHistory(requireContext()) val watchHistory = PreferenceHelper.getWatchHistory()
val watchHistoryAdapter = WatchHistoryAdapter(watchHistory, childFragmentManager)
binding.watchHistoryRecView.adapter = watchHistoryAdapter
binding.clearHistory.setOnClickListener { if (watchHistory.isEmpty()) return
PreferenceHelper.removePreference(requireContext(), "watch_history")
watchHistoryAdapter.clear() // reversed order
binding.watchHistoryRecView.layoutManager = LinearLayoutManager(requireContext()).apply {
reverseLayout = true
stackFromEnd = true
} }
// reverse order val watchHistoryAdapter = WatchHistoryAdapter(
val linearLayoutManager = LinearLayoutManager(view.context) watchHistory,
linearLayoutManager.reverseLayout = true childFragmentManager
linearLayoutManager.stackFromEnd = true )
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) @JsonIgnoreProperties(ignoreUnknown = true)
data class ChapterSegment( data class ChapterSegment(
var title: String?, var title: String? = null,
var image: String?, var image: String? = null,
var start: Long? var start: Long? = null
) { )
constructor() : this("", "", -1)
}

View File

@ -4,17 +4,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class Comment( data class Comment(
val author: String?, val author: String? = null,
val commentId: String?, val commentId: String? = null,
val commentText: String?, val commentText: String? = null,
val commentedTime: String?, val commentedTime: String? = null,
val commentorUrl: String?, val commentorUrl: String? = null,
val repliesPage: String?, val repliesPage: String? = null,
val hearted: Boolean?, val hearted: Boolean? = null,
val likeCount: Int?, val likeCount: Int? = null,
val pinned: Boolean?, val pinned: Boolean? = null,
val thumbnail: String?, val thumbnail: String? = null,
val verified: Boolean? val verified: Boolean? = null
) { )
constructor() : this("", "", "", "", "", "", null, 0, null, "", null)
}

View File

@ -7,6 +7,4 @@ data class CommentsPage(
val comments: MutableList<Comment> = arrayListOf(), val comments: MutableList<Comment> = arrayListOf(),
val disabled: Boolean? = null, val disabled: Boolean? = null,
val nextpage: String? = "" 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) @JsonIgnoreProperties(ignoreUnknown = true)
data class PipedStream( data class PipedStream(
var url: String?, var url: String? = null,
var format: String?, var format: String? = null,
var quality: String?, var quality: String? = null,
var mimeType: String?, var mimeType: String? = null,
var codec: String?, var codec: String? = null,
var videoOnly: Boolean?, var videoOnly: Boolean? = null,
var bitrate: Int?, var bitrate: Int? = null,
var initStart: Int?, var initStart: Int? = null,
var initEnd: Int?, var initEnd: Int? = null,
var indexStart: Int?, var indexStart: Int? = null,
var indexEnd: Int?, var indexEnd: Int? = null,
var width: Int?, var width: Int? = null,
var height: Int?, var height: Int? = null,
var fps: Int? var fps: Int? = null
) { )
constructor() : this("", "", "", "", "", null, -1, -1, -1, -1, -1, -1, -1, -1)
}

View File

@ -4,25 +4,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class SearchItem( data class SearchItem(
var url: String?, var url: String? = null,
var thumbnail: String?, var thumbnail: String? = null,
var uploaderName: String?, var uploaderName: String? = null,
var uploaded: Long?, var uploaded: Long? = null,
var shortDescription: String?, var shortDescription: String? = null,
// Video only attributes // Video only attributes
var title: String?, var title: String? = null,
var uploaderUrl: String?, var uploaderUrl: String? = null,
var uploaderAvatar: String?, var uploaderAvatar: String? = null,
var uploadedDate: String?, var uploadedDate: String? = null,
var duration: Long?, var duration: Long? = null,
var views: Long?, var views: Long? = null,
var uploaderVerified: Boolean?, var uploaderVerified: Boolean? = null,
// Channel and Playlist attributes // Channel and Playlist attributes
var name: String? = null, var name: String? = null,
var description: String? = null, var description: String? = null,
var subscribers: Long? = -1, var subscribers: Long? = -1,
var videos: Long? = -1, var videos: Long? = -1,
var verified: Boolean? = null 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) @JsonIgnoreProperties(ignoreUnknown = true)
data class Segment( data class Segment(
val actionType: String?, val actionType: String? = null,
val category: String?, val category: String? = null,
val segment: List<Float>? val segment: List<Float>? = arrayListOf()
) { )
constructor() : this("", "", arrayListOf())
}

View File

@ -5,6 +5,4 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class Segments( data class Segments(
val segments: MutableList<Segment> = arrayListOf() 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) @JsonIgnoreProperties(ignoreUnknown = true)
data class StreamItem( data class StreamItem(
var url: String?, var url: String? = null,
var title: String?, var title: String? = null,
var thumbnail: String?, var thumbnail: String? = null,
var uploaderName: String?, var uploaderName: String? = null,
var uploaderUrl: String?, var uploaderUrl: String? = null,
var uploaderAvatar: String?, var uploaderAvatar: String? = null,
var uploadedDate: String?, var uploadedDate: String? = null,
var duration: Long?, var duration: Long? = null,
var views: Long?, var views: Long? = null,
var uploaderVerified: Boolean?, var uploaderVerified: Boolean? = null,
var uploaded: Long?, var uploaded: Long? = null,
var shortDescription: String? var shortDescription: String? = null
) { )
constructor() : this("", "", "", "", "", "", "", 0, 0, null, 0, "")
}

View File

@ -15,7 +15,7 @@ data class Streams(
val dash: String?, val dash: String?,
val lbryId: String?, val lbryId: String?,
val uploaderVerified: Boolean?, val uploaderVerified: Boolean?,
val duration: Int?, val duration: Long?,
val views: Long?, val views: Long?,
val likes: Long?, val likes: Long?,
val dislikes: Long?, val dislikes: Long?,

View File

@ -4,11 +4,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class Subtitle( data class Subtitle(
val url: String?, val url: String? = null,
val mimeType: String?, val mimeType: String? = null,
val name: String?, val name: String? = null,
val code: String?, val code: String? = null,
val autoGenerated: Boolean? val autoGenerated: Boolean? = null
) { )
constructor() : this("", "", "", "", 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 package com.github.libretube.obj
data class WatchHistoryItem( data class WatchHistoryItem(
val videoId: String?, val videoId: String? = null,
val title: String?, val title: String? = null,
val uploadDate: String?, val uploadDate: String? = null,
val uploader: String?, val uploader: String? = null,
val uploaderUrl: String?, val uploaderUrl: String? = null,
val uploaderAvatar: String?, val uploaderAvatar: String? = null,
val thumbnailUrl: String?, val thumbnailUrl: String? = null,
val duration: Int? val duration: Long? = null
) )

View File

@ -1,6 +1,6 @@
package com.github.libretube.obj package com.github.libretube.obj
data class WatchPosition( data class WatchPosition(
val videoId: String, val videoId: String = "",
val position: Long val position: Long = 0L
) )

View File

@ -2,13 +2,12 @@ package com.github.libretube.preferences
import android.os.Bundle import android.os.Bundle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity 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 import com.google.android.material.dialog.MaterialAlertDialogBuilder
class AdvancedSettings : PreferenceFragmentCompat() { class AdvancedSettings : MaterialPreferenceFragment() {
val TAG = "AdvancedSettings" val TAG = "AdvancedSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -17,22 +16,7 @@ class AdvancedSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.advanced)) settingsActivity.changeTopBarText(getString(R.string.advanced))
// clear search history val resetSettings = findPreference<Preference>(PreferenceKeys.RESET_SETTINGS)
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")
resetSettings?.setOnPreferenceClickListener { resetSettings?.setOnPreferenceClickListener {
showResetDialog() showResetDialog()
true true
@ -41,19 +25,18 @@ class AdvancedSettings : PreferenceFragmentCompat() {
private fun showResetDialog() { private fun showResetDialog() {
MaterialAlertDialogBuilder(requireContext()) 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) .setTitle(R.string.reset)
.setMessage(R.string.reset_message) .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() .show()
} }
} }

View File

@ -1,16 +1,21 @@
package com.github.libretube.preferences package com.github.libretube.preferences
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity import com.github.libretube.activities.SettingsActivity
import com.github.libretube.dialogs.RequireRestartDialog import com.github.libretube.dialogs.RequireRestartDialog
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
import com.github.libretube.views.MaterialPreferenceFragment
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
class AppearanceSettings : PreferenceFragmentCompat() { class AppearanceSettings : MaterialPreferenceFragment() {
private val TAG = "AppearanceSettings" private val TAG = "AppearanceSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.appearance_settings, rootKey) setPreferencesFromResource(R.xml.appearance_settings, rootKey)
@ -18,52 +23,59 @@ class AppearanceSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.appearance)) settingsActivity.changeTopBarText(getString(R.string.appearance))
val themeToggle = findPreference<ListPreference>("theme_toggle") val themeToggle = findPreference<ListPreference>(PreferenceKeys.THEME_MODE)
themeToggle?.setOnPreferenceChangeListener { _, _ -> themeToggle?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog() val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog") restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true true
} }
val pureTheme = findPreference<SwitchPreferenceCompat>("pure_theme") val pureTheme = findPreference<SwitchPreferenceCompat>(PreferenceKeys.PURE_THEME)
pureTheme?.setOnPreferenceChangeListener { _, _ -> pureTheme?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog() val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog") restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true true
} }
val accentColor = findPreference<ListPreference>("accent_color") val accentColor = findPreference<ListPreference>(PreferenceKeys.ACCENT_COLOR)
updateAccentColorValues(accentColor!!) updateAccentColorValues(accentColor!!)
accentColor.setOnPreferenceChangeListener { _, _ -> accentColor.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog() val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog") restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
true true
} }
val iconChange = findPreference<ListPreference>("icon_change") val iconChange = findPreference<ListPreference>(PreferenceKeys.APP_ICON)
iconChange?.setOnPreferenceChangeListener { _, newValue -> iconChange?.setOnPreferenceChangeListener { _, newValue ->
ThemeHelper.changeIcon(requireContext(), newValue.toString()) ThemeHelper.changeIcon(requireContext(), newValue.toString())
true true
} }
val gridColumns = findPreference<ListPreference>("grid") val labelVisibilityMode = findPreference<ListPreference>(PreferenceKeys.LABEL_VISIBILITY)
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")
labelVisibilityMode?.setOnPreferenceChangeListener { _, _ -> labelVisibilityMode?.setOnPreferenceChangeListener { _, _ ->
val restartDialog = RequireRestartDialog() 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 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 package com.github.libretube.preferences
import android.Manifest
import android.content.ContentResolver
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity 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.DeleteAccountDialog
import com.github.libretube.dialogs.LoginDialog import com.github.libretube.dialogs.LoginDialog
import com.github.libretube.dialogs.LogoutDialog 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 com.github.libretube.util.RetrofitInstance
import org.json.JSONObject import com.github.libretube.views.MaterialPreferenceFragment
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
class InstanceSettings : PreferenceFragmentCompat() { class InstanceSettings : MaterialPreferenceFragment() {
val TAG = "InstanceSettings" 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?) { override fun onCreate(savedInstanceState: Bundle?) {
MainSettings.getContent = getContent =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> registerForActivityResult(
if (uri != null) { ActivityResultContracts.GetContent()
try { ) { uri: Uri? ->
// Open a specific media item using ParcelFileDescriptor. ImportHelper(requireActivity()).importSubscriptions(uri)
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
}
}
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()
}
}
} }
createFile = registerForActivityResult(
ActivityResultContracts.CreateDocument()
) { uri: Uri? ->
ImportHelper(requireActivity()).exportSubscriptions(uri)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
@ -115,25 +53,24 @@ class InstanceSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.instance)) settingsActivity.changeTopBarText(getString(R.string.instance))
val instance = findPreference<ListPreference>("selectInstance") val instance = findPreference<ListPreference>(PreferenceKeys.FETCH_INSTANCE)
// fetchInstance() // fetchInstance()
initCustomInstances(instance!!) initCustomInstances(instance!!)
instance.setOnPreferenceChangeListener { _, newValue -> instance.setOnPreferenceChangeListener { _, newValue ->
val restartDialog = RequireRestartDialog()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
RetrofitInstance.url = newValue.toString() RetrofitInstance.url = newValue.toString()
if (!PreferenceHelper.getBoolean(requireContext(), "auth_instance_toggle", false)) { if (!PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
RetrofitInstance.authUrl = newValue.toString() RetrofitInstance.authUrl = newValue.toString()
logout() logout()
} }
RetrofitInstance.lazyMgr.reset() RetrofitInstance.lazyMgr.reset()
activity?.recreate()
true true
} }
val authInstance = findPreference<ListPreference>("selectAuthInstance") val authInstance = findPreference<ListPreference>(PreferenceKeys.AUTH_INSTANCE)
initCustomInstances(authInstance!!) initCustomInstances(authInstance!!)
// hide auth instance if option deselected // 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.isVisible = false
} }
authInstance.setOnPreferenceChangeListener { _, newValue -> authInstance.setOnPreferenceChangeListener { _, newValue ->
@ -141,226 +78,132 @@ class InstanceSettings : PreferenceFragmentCompat() {
RetrofitInstance.authUrl = newValue.toString() RetrofitInstance.authUrl = newValue.toString()
RetrofitInstance.lazyMgr.reset() RetrofitInstance.lazyMgr.reset()
logout() logout()
val restartDialog = RequireRestartDialog() activity?.recreate()
restartDialog.show(childFragmentManager, "RequireRestartDialog")
true true
} }
val authInstanceToggle = findPreference<SwitchPreferenceCompat>("auth_instance_toggle") val authInstanceToggle =
findPreference<SwitchPreferenceCompat>(PreferenceKeys.AUTH_INSTANCE_TOGGLE)
authInstanceToggle?.setOnPreferenceChangeListener { _, newValue -> authInstanceToggle?.setOnPreferenceChangeListener { _, newValue ->
authInstance.isVisible = newValue == true authInstance.isVisible = newValue == true
logout() logout()
// either use new auth url or the normal api url if auth instance disabled // either use new auth url or the normal api url if auth instance disabled
RetrofitInstance.authUrl = if (newValue == false) RetrofitInstance.url RetrofitInstance.authUrl = if (newValue == false) RetrofitInstance.url
else authInstance.value else authInstance.value
val restartDialog = RequireRestartDialog() RetrofitInstance.lazyMgr.reset()
restartDialog.show(childFragmentManager, "RequireRestartDialog") activity?.recreate()
true true
} }
val customInstance = findPreference<Preference>("customInstance") val customInstance = findPreference<Preference>(PreferenceKeys.CUSTOM_INSTANCE)
customInstance?.setOnPreferenceClickListener { customInstance?.setOnPreferenceClickListener {
val newFragment = CustomInstanceDialog() val newFragment = CustomInstanceDialog()
newFragment.show(childFragmentManager, "CustomInstanceDialog") newFragment.show(childFragmentManager, CustomInstanceDialog::class.java.name)
true true
} }
val clearCustomInstances = findPreference<Preference>("clearCustomInstances") val clearCustomInstances = findPreference<Preference>(PreferenceKeys.CLEAR_CUSTOM_INSTANCES)
clearCustomInstances?.setOnPreferenceClickListener { clearCustomInstances?.setOnPreferenceClickListener {
PreferenceHelper.removePreference(requireContext(), "customInstances") PreferenceHelper.removePreference("customInstances")
val intent = Intent(context, SettingsActivity::class.java) val intent = Intent(context, SettingsActivity::class.java)
startActivity(intent) startActivity(intent)
true true
} }
val login = findPreference<Preference>("login_register") val login = findPreference<Preference>(PreferenceKeys.LOGIN_REGISTER)
val token = PreferenceHelper.getToken(requireContext()) val token = PreferenceHelper.getToken()
if (token != "") login?.setTitle(R.string.logout) if (token != "") login?.setTitle(R.string.logout)
login?.setOnPreferenceClickListener { login?.setOnPreferenceClickListener {
if (token == "") { if (token == "") {
val newFragment = LoginDialog() val newFragment = LoginDialog()
newFragment.show(childFragmentManager, "Login") newFragment.show(childFragmentManager, LoginDialog::class.java.name)
} else { } else {
val newFragment = LogoutDialog() val newFragment = LogoutDialog()
newFragment.show(childFragmentManager, "Logout") newFragment.show(childFragmentManager, LogoutDialog::class.java.name)
} }
true true
} }
val deleteAccount = findPreference<Preference>("delete_account") val deleteAccount = findPreference<Preference>(PreferenceKeys.DELETE_ACCOUNT)
deleteAccount?.setOnPreferenceClickListener { deleteAccount?.setOnPreferenceClickListener {
val token = PreferenceHelper.getToken(requireContext()) val token = PreferenceHelper.getToken()
if (token != "") { if (token != "") {
val newFragment = DeleteAccountDialog() val newFragment = DeleteAccountDialog()
newFragment.show(childFragmentManager, "DeleteAccountDialog") newFragment.show(childFragmentManager, DeleteAccountDialog::class.java.name)
} else { } else {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
} }
true true
} }
val importFromYt = findPreference<Preference>("import_from_yt") val importSubscriptions = findPreference<Preference>(PreferenceKeys.IMPORT_SUBS)
importFromYt?.setOnPreferenceClickListener { importSubscriptions?.setOnPreferenceClickListener {
importSubscriptions() // 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 true
} }
} }
private fun initCustomInstances(instancePref: ListPreference) { private fun initCustomInstances(instancePref: ListPreference) {
val customInstances = PreferenceHelper.getCustomInstances(requireContext())
var instanceNames = resources.getStringArray(R.array.instances)
var instanceValues = resources.getStringArray(R.array.instancesValue)
customInstances.forEach { instance ->
instanceNames += instance.name
instanceValues += instance.apiUrl
}
// add custom instances to the list preference
instancePref.entries = instanceNames
instancePref.entryValues = instanceValues
instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
val text = preference.entry
if (TextUtils.isEmpty(text)) {
"kavin.rocks (Official)"
} else {
text
}
}
}
private fun logout() {
PreferenceHelper.setToken(requireContext(), "")
Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
}
private fun fetchInstance() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val customInstances = PreferenceHelper.getCustomInstances()
val instanceNames = arrayListOf<String>()
val instanceValues = arrayListOf<String>()
// fetch official public instances
val response = try { val response = try {
RetrofitInstance.api.getInstances("https://instances.tokhmi.xyz/") 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) { } catch (e: Exception) {
Log.e("settings", e.toString()) e.printStackTrace()
return@launchWhenCreated emptyList()
}
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 response.forEach {
if (it.name != null && it.api_url != null) {
instanceNames += it.name!!
instanceValues += it.api_url!!
}
}
customInstances.forEach { instance ->
instanceNames += instance.name
instanceValues += instance.apiUrl
}
val entries = listEntries.toTypedArray<CharSequence>()
val entryValues = listEntryValues.toTypedArray<CharSequence>()
runOnUiThread { runOnUiThread {
val instance = findPreference<ListPreference>("selectInstance") // add custom instances to the list preference
instance?.entries = entries instancePref.entries = instanceNames.toTypedArray()
instance?.entryValues = entryValues instancePref.entryValues = instanceValues.toTypedArray()
instance?.summaryProvider = instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference -> Preference.SummaryProvider<ListPreference> { preference ->
val text = preference.entry preference.entry
if (TextUtils.isEmpty(text)) {
"kavin.rocks (Official)"
} else {
text
}
} }
} }
} }
} }
private fun logout() {
PreferenceHelper.setToken("")
Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
}
private fun Fragment?.runOnUiThread(action: () -> Unit) { private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return this ?: return
if (!isAdded) return // Fragment not attached to an Activity if (!isAdded) return // Fragment not attached to an Activity
activity?.runOnUiThread(action) 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 package com.github.libretube.preferences
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.BuildConfig import com.github.libretube.BuildConfig
import com.github.libretube.Globals
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.dialogs.RequireRestartDialog import com.github.libretube.activities.SettingsActivity
import com.github.libretube.util.ThemeHelper import com.github.libretube.dialogs.UpdateDialog
import com.github.libretube.util.checkUpdate 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" val TAG = "SettingsFragment"
companion object {
lateinit var getContent: ActivityResultLauncher<String>
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey) setPreferencesFromResource(R.xml.settings, rootKey)
val region = findPreference<Preference>("region") val general = findPreference<Preference>("general")
region?.setOnPreferenceChangeListener { _, _ -> general?.setOnPreferenceClickListener {
val restartDialog = RequireRestartDialog() val newFragment = GeneralSettings()
restartDialog.show(childFragmentManager, "RequireRestartDialog") navigateToSettingsFragment(newFragment)
true
}
val language = findPreference<ListPreference>("language")
language?.setOnPreferenceChangeListener { _, _ ->
ThemeHelper.restartMainActivity(requireContext())
true true
} }
@ -64,6 +55,20 @@ class MainSettings : PreferenceFragmentCompat() {
true 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") val advanced = findPreference<Preference>("advanced")
advanced?.setOnPreferenceClickListener { advanced?.setOnPreferenceClickListener {
val newFragment = AdvancedSettings() val newFragment = AdvancedSettings()
@ -72,29 +77,48 @@ class MainSettings : PreferenceFragmentCompat() {
} }
val update = findPreference<Preference>("update") 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 { update?.setOnPreferenceClickListener {
checkUpdate(childFragmentManager) CoroutineScope(Dispatchers.IO).launch {
true // check for update
} val updateInfo = UpdateChecker.getLatestReleaseInfo()
if (updateInfo?.name == null) {
val about = findPreference<Preference>("about") // request failed
about?.setOnPreferenceClickListener { val settingsActivity = activity as SettingsActivity
val newFragment = AboutFragment() val snackBar = Snackbar
navigateToSettingsFragment(newFragment) .make(
true settingsActivity.binding.root,
} R.string.unknown_error,
Snackbar.LENGTH_SHORT
val community = findPreference<Preference>("community") )
community?.setOnPreferenceClickListener { snackBar.show()
val newFragment = CommunityFragment() } else if (BuildConfig.VERSION_NAME != updateInfo.name) {
navigateToSettingsFragment(newFragment) // show the UpdateAvailableDialog if there's an update available
val updateAvailableDialog = UpdateDialog(updateInfo)
updateAvailableDialog.show(childFragmentManager, UpdateDialog::class.java.name)
} else {
// otherwise show the no update available snackBar
val settingsActivity = activity as SettingsActivity
val snackBar = Snackbar
.make(
settingsActivity.binding.root,
R.string.app_uptodate,
Snackbar.LENGTH_SHORT
)
snackBar.show()
}
}
true true
} }
} }
private fun navigateToSettingsFragment(newFragment: Fragment) { private fun navigateToSettingsFragment(newFragment: Fragment) {
Globals.isCurrentViewMainSettings = false
parentFragmentManager.beginTransaction() parentFragmentManager.beginTransaction()
.replace(R.id.settings, newFragment) .replace(R.id.settings, newFragment)
.commitNow() .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 android.os.Bundle
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity import com.github.libretube.activities.SettingsActivity
import com.github.libretube.views.MaterialPreferenceFragment
import java.util.*
class PlayerSettings : PreferenceFragmentCompat() { class PlayerSettings : MaterialPreferenceFragment() {
val TAG = "PlayerSettings" val TAG = "PlayerSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -16,13 +18,14 @@ class PlayerSettings : PreferenceFragmentCompat() {
val settingsActivity = activity as SettingsActivity val settingsActivity = activity as SettingsActivity
settingsActivity.changeTopBarText(getString(R.string.audio_video)) settingsActivity.changeTopBarText(getString(R.string.audio_video))
val playerOrientation = findPreference<ListPreference>("fullscreen_orientation") val playerOrientation =
val autoRotateToFullscreen = findPreference<SwitchPreferenceCompat>("auto_fullscreen") findPreference<ListPreference>(PreferenceKeys.FULLSCREEN_ORIENTATION)
val autoRotateToFullscreen =
findPreference<SwitchPreferenceCompat>(PreferenceKeys.AUTO_FULLSCREEN)
// only show the player orientation option if auto fullscreen is disabled // only show the player orientation option if auto fullscreen is disabled
playerOrientation?.isEnabled != PreferenceHelper.getBoolean( playerOrientation?.isEnabled != PreferenceHelper.getBoolean(
requireContext(), PreferenceKeys.AUTO_FULLSCREEN,
"auto_fullscreen",
false false
) )
@ -30,5 +33,26 @@ class PlayerSettings : PreferenceFragmentCompat() {
playerOrientation?.isEnabled = newValue != true playerOrientation?.isEnabled = newValue != true
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.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceManager 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.CustomInstance
import com.github.libretube.obj.Streams import com.github.libretube.obj.Streams
import com.github.libretube.obj.WatchHistoryItem import com.github.libretube.obj.WatchHistoryItem
import com.github.libretube.obj.WatchPosition import com.github.libretube.obj.WatchPosition
import com.google.common.reflect.TypeToken import com.github.libretube.util.toID
import com.google.gson.Gson
import java.lang.reflect.Type
object PreferenceHelper { object PreferenceHelper {
private val TAG = "PreferenceHelper" private val TAG = "PreferenceHelper"
fun setString(context: Context, key: String?, value: String?) { private lateinit var prefContext: Context
val editor = getDefaultSharedPreferencesEditor(context) private lateinit var settings: SharedPreferences
editor.putString(key, value) private lateinit var editor: SharedPreferences.Editor
editor.apply() 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) { fun getString(key: String?, defValue: String?): String {
val editor = getDefaultSharedPreferencesEditor(context) return settings.getString(key, defValue)!!
editor.putInt(key, value)
editor.apply()
} }
fun setLong(context: Context, key: String?, value: Long) { fun getBoolean(key: String?, defValue: Boolean): Boolean {
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)
return settings.getBoolean(key, defValue) return settings.getBoolean(key, defValue)
} }
fun clearPreferences(context: Context) { fun clearPreferences() {
val editor = getDefaultSharedPreferencesEditor(context)
editor.clear().apply() editor.clear().apply()
} }
fun removePreference(context: Context, value: String?) { fun removePreference(value: String?) {
val editor = getDefaultSharedPreferencesEditor(context)
editor.remove(value).apply() editor.remove(value).apply()
} }
fun getToken(context: Context): String { fun getToken(): String {
val sharedPref = context.getSharedPreferences("token", Context.MODE_PRIVATE) val sharedPref = prefContext.getSharedPreferences("token", Context.MODE_PRIVATE)
return sharedPref?.getString("token", "")!! return sharedPref?.getString("token", "")!!
} }
fun setToken(context: Context, newValue: String) { fun setToken(newValue: String) {
val editor = context.getSharedPreferences("token", Context.MODE_PRIVATE).edit() val editor = prefContext.getSharedPreferences("token", Context.MODE_PRIVATE).edit()
editor.putString("token", newValue).apply() editor.putString("token", newValue).apply()
} }
fun getUsername(context: Context): String { fun getUsername(): String {
val sharedPref = context.getSharedPreferences("username", Context.MODE_PRIVATE) val sharedPref = prefContext.getSharedPreferences("username", Context.MODE_PRIVATE)
return sharedPref.getString("username", "")!! return sharedPref.getString("username", "")!!
} }
fun setUsername(context: Context, newValue: String) { fun setUsername(newValue: String) {
val editor = context.getSharedPreferences("username", Context.MODE_PRIVATE).edit() val editor = prefContext.getSharedPreferences("username", Context.MODE_PRIVATE).edit()
editor.putString("username", newValue).apply() editor.putString("username", newValue).apply()
} }
fun saveCustomInstance(context: Context, customInstance: CustomInstance) { fun saveCustomInstance(customInstance: CustomInstance) {
val editor = getDefaultSharedPreferencesEditor(context) val customInstancesList = getCustomInstances()
val gson = Gson()
val customInstancesList = getCustomInstances(context)
customInstancesList += customInstance customInstancesList += customInstance
val json = gson.toJson(customInstancesList) val json = mapper.writeValueAsString(customInstancesList)
editor.putString("customInstances", json).apply() editor.putString("customInstances", json).apply()
} }
fun getCustomInstances(context: Context): ArrayList<CustomInstance> { fun getCustomInstances(): ArrayList<CustomInstance> {
val settings = getDefaultSharedPreferences(context)
val gson = Gson()
val json: String = settings.getString("customInstances", "")!! 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 { return try {
gson.fromJson(json, type) mapper.readValue(json, type)
} catch (e: Exception) { } catch (e: Exception) {
arrayListOf() arrayListOf()
} }
} }
fun getHistory(context: Context): List<String> { fun getSearchHistory(): List<String> {
return try { return try {
val settings = getDefaultSharedPreferences(context) val json = settings.getString("search_history", "")!!
val set: Set<String> = settings.getStringSet("search_history", HashSet())!! val type = object : TypeReference<List<String>>() {}
set.toList() return mapper.readValue(json, type)
} catch (e: Exception) { } catch (e: Exception) {
emptyList() emptyList()
} }
} }
fun saveHistory(context: Context, historyList: List<String>) { fun saveToSearchHistory(query: String) {
val editor = getDefaultSharedPreferencesEditor(context) val historyList = getSearchHistory().toMutableList()
val set: Set<String> = HashSet(historyList)
editor.putStringSet("search_history", set).apply() if ((historyList.contains(query))) {
// remove from history list if already contained
historyList -= query
}
// append new query to history
historyList.add(0, query)
if (historyList.size > 10) {
historyList.removeAt(historyList.size - 1)
}
updateSearchHistory(historyList)
} }
fun addToWatchHistory(context: Context, videoId: String, streams: Streams) { fun removeFromSearchHistory(query: String) {
val editor = getDefaultSharedPreferencesEditor(context) val historyList = getSearchHistory().toMutableList()
val gson = Gson() 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( val watchHistoryItem = WatchHistoryItem(
videoId, videoId,
streams.title, streams.title,
streams.uploadDate, streams.uploadDate,
streams.uploader, streams.uploader,
streams.uploaderUrl?.replace("/channel/", ""), streams.uploaderUrl.toID(),
streams.uploaderAvatar, streams.uploaderAvatar,
streams.thumbnailUrl, streams.thumbnailUrl,
streams.duration 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 var indexToRemove: Int? = null
watchHistory.forEachIndexed { index, item -> watchHistory.forEachIndexed { index, item ->
if (item.videoId == videoId) indexToRemove = index if (item.videoId == videoId) indexToRemove = index
} }
if (indexToRemove != null) watchHistory.removeAt(indexToRemove!!) if (indexToRemove == null) return
watchHistory.removeAt(indexToRemove!!)
watchHistory += watchHistoryItem val json = mapper.writeValueAsString(watchHistory)
editor.putString("watch_history", json).commit()
val json = gson.toJson(watchHistory)
editor.putString("watch_history", json).apply()
} }
fun getWatchHistory(context: Context): ArrayList<WatchHistoryItem> { fun removeFromWatchHistory(position: Int) {
val settings = getDefaultSharedPreferences(context) val watchHistory = getWatchHistory()
val gson = Gson() 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 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 { return try {
gson.fromJson(json, type) mapper.readValue(json, type)
} catch (e: Exception) { } catch (e: Exception) {
arrayListOf() arrayListOf()
} }
} }
fun saveWatchPosition(context: Context, videoId: String, position: Long) { fun saveWatchPosition(videoId: String, position: Long) {
val editor = getDefaultSharedPreferencesEditor(context) val watchPositions = getWatchPositions()
val watchPositions = getWatchPositions(context)
val watchPositionItem = WatchPosition(videoId, position) val watchPositionItem = WatchPosition(videoId, position)
var indexToRemove: Int? = null var indexToRemove: Int? = null
@ -184,15 +200,12 @@ object PreferenceHelper {
watchPositions += watchPositionItem watchPositions += watchPositionItem
val gson = Gson() val json = mapper.writeValueAsString(watchPositions)
val json = gson.toJson(watchPositions)
editor.putString("watch_positions", json).commit() editor.putString("watch_positions", json).commit()
} }
fun removeWatchPosition(context: Context, videoId: String) { fun removeWatchPosition(videoId: String) {
val editor = getDefaultSharedPreferencesEditor(context) val watchPositions = getWatchPositions()
val watchPositions = getWatchPositions(context)
var indexToRemove: Int? = null var indexToRemove: Int? = null
watchPositions.forEachIndexed { index, item -> watchPositions.forEachIndexed { index, item ->
@ -201,28 +214,56 @@ object PreferenceHelper {
if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!) if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!)
val gson = Gson() val json = mapper.writeValueAsString(watchPositions)
val json = gson.toJson(watchPositions)
editor.putString("watch_positions", json).commit() editor.putString("watch_positions", json).commit()
} }
fun getWatchPositions(context: Context): ArrayList<WatchPosition> { fun getWatchPositions(): ArrayList<WatchPosition> {
val settings = getDefaultSharedPreferences(context)
val gson = Gson()
val json: String = settings.getString("watch_positions", "")!! 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 { return try {
gson.fromJson(json, type) mapper.readValue(json, type)
} catch (e: Exception) { } catch (e: Exception) {
arrayListOf() 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 { private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context) 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 package com.github.libretube.preferences
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity import com.github.libretube.activities.SettingsActivity
import com.github.libretube.views.MaterialPreferenceFragment
class SponsorBlockSettings : PreferenceFragmentCompat() { class SponsorBlockSettings : MaterialPreferenceFragment() {
private val TAG = "SponsorBlockSettings" private val TAG = "SponsorBlockSettings"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 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.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.annotation.Nullable import androidx.annotation.Nullable
import com.github.libretube.PLAYER_NOTIFICATION_ID
class ClosingService : Service() { class ClosingService : Service() {
private val TAG = "ClosingService" private val TAG = "ClosingService"
@ -20,10 +20,9 @@ class ClosingService : Service() {
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) 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 val nManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nManager.cancelAll() nManager.cancel(PLAYER_NOTIFICATION_ID)
Log.e(TAG, "closed")
// Destroy the service // Destroy the service
stopSelf() stopSelf()

View File

@ -17,25 +17,26 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat 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.R
import com.github.libretube.obj.DownloadType import com.github.libretube.obj.DownloadType
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import java.io.File import java.io.File
var IS_DOWNLOAD_RUNNING = false
class DownloadService : Service() { class DownloadService : Service() {
val TAG = "DownloadService" val TAG = "DownloadService"
private lateinit var notification: NotificationCompat.Builder private lateinit var notification: NotificationCompat.Builder
private var downloadId: Long = -1 private var downloadId: Long = -1
private lateinit var videoId: String private lateinit var videoName: String
private lateinit var videoUrl: String private lateinit var videoUrl: String
private lateinit var audioUrl: String private lateinit var audioUrl: String
private lateinit var extension: String
private var duration: Int = 0
private var downloadType: Int = 3 private var downloadType: Int = 3
private lateinit var audioDir: File private lateinit var audioDir: File
@ -44,17 +45,15 @@ class DownloadService : Service() {
private lateinit var tempDir: File private lateinit var tempDir: File
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
IS_DOWNLOAD_RUNNING = true Globals.IS_DOWNLOAD_RUNNING = true
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
videoId = intent?.getStringExtra("videoId")!! videoName = intent?.getStringExtra("videoName")!!
videoUrl = intent.getStringExtra("videoUrl")!! videoUrl = intent.getStringExtra("videoUrl")!!
audioUrl = intent.getStringExtra("audioUrl")!! audioUrl = intent.getStringExtra("audioUrl")!!
duration = intent.getIntExtra("duration", 1)
extension = PreferenceHelper.getString(this, "video_format", ".mp4")!! downloadType = if (audioUrl != "") DownloadType.AUDIO
downloadType = if (audioUrl != "" && videoUrl != "") DownloadType.MUX
else if (audioUrl != "") DownloadType.AUDIO
else if (videoUrl != "") DownloadType.VIDEO else if (videoUrl != "") DownloadType.VIDEO
else DownloadType.NONE else DownloadType.NONE
if (downloadType != DownloadType.NONE) { if (downloadType != DownloadType.NONE) {
@ -86,8 +85,8 @@ class DownloadService : Service() {
Log.e(TAG, "Directory already have") Log.e(TAG, "Directory already have")
} }
val downloadLocationPref = PreferenceHelper.getString(this, "download_location", "") val downloadLocationPref = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_LOCATION, "")
val folderName = PreferenceHelper.getString(this, "download_folder", "LibreTube") val folderName = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_FOLDER, "LibreTube")
val location = when (downloadLocationPref) { val location = when (downloadLocationPref) {
"downloads" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) "downloads" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
@ -111,18 +110,8 @@ class DownloadService : Service() {
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
) )
when (downloadType) { 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 -> { DownloadType.VIDEO -> {
videoDir = File(libretubeDir, "$videoId-video") videoDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest( downloadId = downloadManagerRequest(
getString(R.string.video), getString(R.string.video),
getString(R.string.downloading), getString(R.string.downloading),
@ -131,7 +120,7 @@ class DownloadService : Service() {
) )
} }
DownloadType.AUDIO -> { DownloadType.AUDIO -> {
audioDir = File(libretubeDir, "$videoId-audio") audioDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest( downloadId = downloadManagerRequest(
getString(R.string.audio), getString(R.string.audio),
getString(R.string.downloading), getString(R.string.downloading),
@ -142,6 +131,7 @@ class DownloadService : Service() {
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e(TAG, "download error $e") Log.e(TAG, "download error $e")
downloadFailedNotification()
} }
} }
@ -162,8 +152,6 @@ class DownloadService : Service() {
downloadSucceededNotification() downloadSucceededNotification()
onDestroy() onDestroy()
} }
} else {
muxDownloadedMedia()
} }
} }
} }
@ -196,7 +184,7 @@ class DownloadService : Service() {
} }
// Creating a notification and setting its various attributes // Creating a notification and setting its various attributes
notification = notification =
NotificationCompat.Builder(this@DownloadService, "download_service") NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download) .setSmallIcon(R.drawable.ic_download)
.setContentTitle("LibreTube") .setContentTitle("LibreTube")
.setContentText(getString(R.string.downloading)) .setContentText(getString(R.string.downloading))
@ -206,70 +194,31 @@ class DownloadService : Service() {
.setProgress(100, 0, true) .setProgress(100, 0, true)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
startForeground(2, notification.build()) startForeground(DOWNLOAD_PENDING_NOTIFICATION_ID, notification.build())
} }
private fun downloadFailedNotification() { 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) .setSmallIcon(R.drawable.ic_download)
.setContentTitle(resources.getString(R.string.downloadfailed)) .setContentTitle(resources.getString(R.string.downloadfailed))
.setContentText(getString(R.string.fail)) .setContentText(getString(R.string.fail))
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) { with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define // 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() { private fun downloadSucceededNotification() {
Log.i(TAG, "Download succeeded") 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) .setSmallIcon(R.drawable.ic_download)
.setContentTitle(resources.getString(R.string.success)) .setContentTitle(resources.getString(R.string.success))
.setContentText(getString(R.string.fail)) .setContentText(getString(R.string.downloadsucceeded))
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) { with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define // notificationId is a unique int for each notification that you must define
notify(4, builder.build()) notify(DOWNLOAD_SUCCESS_NOTIFICATION_ID, 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())
}*/
} }
} }
@ -279,7 +228,7 @@ class DownloadService : Service() {
} catch (e: Exception) { } catch (e: Exception) {
} }
IS_DOWNLOAD_RUNNING = false Globals.IS_DOWNLOAD_RUNNING = false
Log.d(TAG, "dl finished!") Log.d(TAG, "dl finished!")
stopForeground(true) stopForeground(true)
stopService(Intent(this@DownloadService, DownloadService::class.java)) 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