mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 23:40:33 +05:30
Merge branch 'libre-tube:master' into master
This commit is contained in:
commit
6ccec878d5
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,2 +1,2 @@
|
||||
patreon: LibreTubeTeam
|
||||
custom: https://libre-tube.github.io#donate
|
||||
custom: https://github.com/libre-tube/LibreTube#-donate
|
||||
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
35
README.md
35
README.md
@ -1,21 +1,29 @@
|
||||
<div align="center">
|
||||
<img src="https://libre-tube.github.io/assets/gh-banner.png" width="auto" height="auto" alt="LibreTube">
|
||||
<img src="https://libre-tube.github.io/images/gh-banner.png" width="auto" height="auto" alt="LibreTube">
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://matrix.to/#/#LibreTube:matrix.org)
|
||||
[](https://t.me/libretube)
|
||||
[](https://twitter.com/libretube)
|
||||
[](https://www.reddit.com/r/Libretube/)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://matrix.to/#/#LibreTube:matrix.org)
|
||||
[](https://t.me/libretube)
|
||||
[](https://twitter.com/libretube)
|
||||
[](https://www.reddit.com/r/Libretube/)
|
||||
|
||||
</div><div align="center" style="width:100%; display:flex; justify-content:space-between;">
|
||||
|
||||
[<img src="https://libre-tube.github.io/assets/fdrload.png" alt="Get it on F-Droid" width="30%">](https://f-droid.org/en/packages/com.github.libretube/)
|
||||
[<img src="https://libre-tube.github.io/assets/izzyload.png" alt="Get it on IzzyOnDroid" width="30%">](https://apt.izzysoft.de/fdroid/index/apk/com.github.libretube)<br/>
|
||||
[<img src="https://libre-tube.github.io/assets/ghload.png" alt="Get it on GitHub" width="30%">](https://github.com/libre-tube/LibreTube/releases/latest)
|
||||
[<img src="https://libre-tube.github.io/assets/tgload.png" alt="Get it on GitHub" width="30%">](https://t.me/LibreTube)
|
||||
[<img src="https://libre-tube.github.io/images/fdrload.png" alt="Get it on F-Droid" width="30%">](https://f-droid.org/en/packages/com.github.libretube/)
|
||||
[<img src="https://libre-tube.github.io/images/izzyload.png" alt="Get it on IzzyOnDroid" width="30%">](https://apt.izzysoft.de/fdroid/index/apk/com.github.libretube)<br/>
|
||||
[<img src="https://libre-tube.github.io/images/ghload.png" alt="Get it on GitHub" width="30%">](https://github.com/libre-tube/LibreTube/releases/latest)
|
||||
[<img src="https://libre-tube.github.io/images/tgload.png" alt="Get it on GitHub" width="30%">](https://t.me/LibreTube)
|
||||
|
||||
</div>
|
||||
|
||||
## 📔 About
|
||||
|
||||
YouTube has an extremely invasive privacy policy which relies on using user data in unethical ways. They store a lot of your personal data - ranging from ideas, music taste, content, political opinions, and much more than you think.
|
||||
|
||||
The project is aimed to improve the users privacy by being independent from Google and bypassing their data collection. Therefore the app is using the [Piped API](https://github.com/TeamPiped/Piped), which uses proxies to circumvent Googles data collection and includes some other additional features.
|
||||
|
||||
> **⚠️ WARNING: This is a beta version, therefore you may encounter bugs. If you do, open an issue via our GitHub repository.**
|
||||
|
||||
## 📱 Screenshots
|
||||
|
||||
<div style="width:100%; display:flex; justify-content:space-between;">
|
||||
@ -47,14 +55,12 @@
|
||||
|
||||
## 😇 Contributing
|
||||
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.The more is done the better it gets!
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. The more is done, the better it gets!
|
||||
|
||||
If creating a pull request, please make sure to format your code (preferred ktlint) before.
|
||||
|
||||
If opening an issue without following the issue template, we will ignore the issue and force close it.
|
||||
|
||||
> **⚠️ WARNING: This is a beta version, therefore you may encounter bugs. If you do, open an issue via our github repository.**
|
||||
|
||||
### 📝 Translation
|
||||
|
||||
<a href="https://hosted.weblate.org/projects/libretube/#languages">
|
||||
@ -73,3 +79,6 @@ If opening an issue without following the issue template, we will ignore the iss
|
||||
|
||||
<a href="https://gitlab.com/libretube/LibreTube">GitLab</a></p>
|
||||
<a href="https://notabug.org/LibreTube/LibreTube">NotABug</a></p>
|
||||
<div align="right">
|
||||
<b><a href="https://github.com/libre-tube/LibreTube#-about">↥ Back to top</a></b>
|
||||
</div>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import java.time.Instant
|
||||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-android-extensions'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -10,8 +13,8 @@ android {
|
||||
applicationId 'com.github.libretube'
|
||||
minSdk 21
|
||||
targetSdk 31
|
||||
versionCode 13
|
||||
versionName '0.3.3'
|
||||
versionCode 16
|
||||
versionName '0.4.2'
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
resValue "string", "app_name", "LibreTube"
|
||||
@ -21,6 +24,16 @@ android {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
// use the date as version for debug builds
|
||||
if (variant.name == 'debug') {
|
||||
variant.outputs.each { output ->
|
||||
output.versionCodeOverride = getUnixTime()
|
||||
output.versionNameOverride = getUnixTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
@ -68,6 +81,7 @@ dependencies {
|
||||
implementation libs.androidx.navigation.fragment
|
||||
implementation libs.androidx.navigation.ui
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.work.runtime
|
||||
|
||||
androidTestImplementation libs.androidx.test.junit
|
||||
androidTestImplementation libs.androidx.test.espressoCore
|
||||
@ -79,15 +93,21 @@ dependencies {
|
||||
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
|
||||
implementation libs.exoplayer.extension.mediasession
|
||||
|
||||
implementation libs.square.picasso
|
||||
implementation libs.square.retrofit
|
||||
implementation libs.square.retrofit.converterJackson
|
||||
// Do not update jackson annotations! It does not supports < API 26.
|
||||
implementation libs.jacksonAnnotations
|
||||
|
||||
implementation libs.mobileffmpeg
|
||||
|
||||
coreLibraryDesugaring libs.desugaring
|
||||
implementation libs.cronet.embedded
|
||||
implementation libs.gson
|
||||
implementation libs.cronet.okhttp
|
||||
implementation libs.coil
|
||||
|
||||
implementation libs.lifecycle.viewmodel
|
||||
implementation libs.lifecycle.runtime
|
||||
implementation libs.lifecycle.livedata
|
||||
}
|
||||
|
||||
static def getUnixTime() {
|
||||
return Instant.now().getEpochSecond()
|
||||
}
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@ -14,7 +14,7 @@
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
@ -22,3 +22,9 @@
|
||||
#uncomment for debug
|
||||
#-keepnames class **
|
||||
-keep class com.github.libretube.obj.** { *; }
|
||||
|
||||
# prevents android from removing it
|
||||
-keep class com.github.libretube.update.** { *; }
|
||||
|
||||
# prevents obfuscation in debug logs
|
||||
-dontobfuscate
|
@ -16,10 +16,23 @@
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 7,
|
||||
"versionName": "0.2.5",
|
||||
"versionCode": 16,
|
||||
"versionName": "0.4.2",
|
||||
"outputFile": "app-x86_64-release.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "arm64-v8a"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 16,
|
||||
"versionName": "0.4.2",
|
||||
"outputFile": "app-arm64-v8a-release.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
@ -29,8 +42,8 @@
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 7,
|
||||
"versionName": "0.2.5",
|
||||
"versionCode": 16,
|
||||
"versionName": "0.4.2",
|
||||
"outputFile": "app-x86-release.apk"
|
||||
},
|
||||
{
|
||||
@ -42,22 +55,9 @@
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 7,
|
||||
"versionName": "0.2.5",
|
||||
"versionCode": 16,
|
||||
"versionName": "0.4.2",
|
||||
"outputFile": "app-armeabi-v7a-release.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "arm64-v8a"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 7,
|
||||
"versionName": "0.2.5",
|
||||
"outputFile": "app-arm64-v8a-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -7,7 +8,9 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
@ -20,17 +23,25 @@
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Purple">
|
||||
<activity
|
||||
android:name=".activities.Player"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="false" />
|
||||
android:theme="@style/Theme.Purple.Pure"
|
||||
tools:targetApi="n">
|
||||
|
||||
<activity
|
||||
android:name=".activities.NoInternetActivity"
|
||||
android:label="@string/noInternet" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.AboutActivity"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.CommunityActivity"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
@ -281,8 +292,17 @@
|
||||
<service
|
||||
android:name=".services.ClosingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false" />
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.UpdateService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.BackgroundMode"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -12,6 +12,7 @@ const val WEBSITE_URL = "https://libre-tube.github.io/"
|
||||
const val DONATE_URL = "https://github.com/libre-tube/LibreTube#donate"
|
||||
const val GITHUB_URL = "https://github.com/libre-tube/LibreTube"
|
||||
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped"
|
||||
const val WEBLATE_URL = "https://hosted.weblate.org/projects/libretube/libretube/"
|
||||
|
||||
/**
|
||||
* Social media links for the community fragment
|
||||
@ -32,3 +33,19 @@ const val YOUTUBE_FRONTEND_URL = "https://www.youtube.com"
|
||||
* Retrofit Instance
|
||||
*/
|
||||
const val PIPED_API_URL = "https://pipedapi.kavin.rocks/"
|
||||
|
||||
/**
|
||||
* Notification IDs
|
||||
*/
|
||||
const val PLAYER_NOTIFICATION_ID = 1
|
||||
const val PUSH_NOTIFICATION_ID = 2
|
||||
const val DOWNLOAD_PENDING_NOTIFICATION_ID = 3
|
||||
const val DOWNLOAD_FAILURE_NOTIFICATION_ID = 4
|
||||
const val DOWNLOAD_SUCCESS_NOTIFICATION_ID = 5
|
||||
|
||||
/**
|
||||
* Notification Channel IDs
|
||||
*/
|
||||
const val DOWNLOAD_CHANNEL_ID = "download_service"
|
||||
const val BACKGROUND_CHANNEL_ID = "background_mode"
|
||||
const val PUSH_CHANNEL_ID = "notification_worker"
|
||||
|
@ -1,7 +1,22 @@
|
||||
package com.github.libretube
|
||||
|
||||
/**
|
||||
* Global variables can be stored here
|
||||
*/
|
||||
object Globals {
|
||||
var isFullScreen = false
|
||||
var isMiniPlayerVisible = false
|
||||
var isCurrentViewMainSettings = true
|
||||
// for the player fragment
|
||||
var IS_FULL_SCREEN = false
|
||||
var MINI_PLAYER_VISIBLE = false
|
||||
|
||||
// for the data saver mode
|
||||
var DATA_SAVER_MODE_ENABLED = false
|
||||
|
||||
// for downloads
|
||||
var IS_DOWNLOAD_RUNNING = false
|
||||
|
||||
// for playlists
|
||||
var SELECTED_PLAYLIST_ID: String? = null
|
||||
|
||||
// history of played videos in the current lifecycle
|
||||
val playingQueue = mutableListOf<String>()
|
||||
}
|
||||
|
@ -4,12 +4,69 @@ import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import com.github.libretube.util.ExceptionHandler
|
||||
import com.github.libretube.util.NotificationHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
|
||||
class MyApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
/**
|
||||
* initialize the needed [NotificationChannel]s for DownloadService and BackgroundMode
|
||||
*/
|
||||
initializeNotificationChannels()
|
||||
|
||||
/**
|
||||
* set the applicationContext as context for the [PreferenceHelper]
|
||||
*/
|
||||
PreferenceHelper.setContext(applicationContext)
|
||||
|
||||
/**
|
||||
* bypassing fileUriExposedException, see https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed
|
||||
*/
|
||||
val builder = VmPolicy.Builder()
|
||||
StrictMode.setVmPolicy(builder.build())
|
||||
|
||||
/**
|
||||
* set the api and the auth api url
|
||||
*/
|
||||
setRetrofitApiUrls()
|
||||
|
||||
/**
|
||||
* initialize the notification listener in the background
|
||||
*/
|
||||
NotificationHelper.enqueueWork(this, ExistingPeriodicWorkPolicy.KEEP)
|
||||
|
||||
/**
|
||||
* Handler for uncaught exceptions
|
||||
*/
|
||||
val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
val exceptionHandler = ExceptionHandler(defaultExceptionHandler)
|
||||
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* set the api urls needed for the [RetrofitInstance]
|
||||
*/
|
||||
private fun setRetrofitApiUrls() {
|
||||
RetrofitInstance.url =
|
||||
PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)
|
||||
// set auth instance
|
||||
RetrofitInstance.authUrl =
|
||||
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
|
||||
PreferenceHelper.getString(
|
||||
PreferenceKeys.AUTH_INSTANCE,
|
||||
PIPED_API_URL
|
||||
)
|
||||
} else {
|
||||
RetrofitInstance.url
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -17,17 +74,23 @@ class MyApp : Application() {
|
||||
*/
|
||||
private fun initializeNotificationChannels() {
|
||||
createNotificationChannel(
|
||||
"download_service",
|
||||
"Download Service",
|
||||
DOWNLOAD_CHANNEL_ID,
|
||||
"Download Service",
|
||||
"Shows a notification when downloading media.",
|
||||
NotificationManager.IMPORTANCE_NONE
|
||||
)
|
||||
createNotificationChannel(
|
||||
"background_mode",
|
||||
BACKGROUND_CHANNEL_ID,
|
||||
"Background Mode",
|
||||
"Shows a notification with buttons to control the audio player",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
createNotificationChannel(
|
||||
PUSH_CHANNEL_ID,
|
||||
"Notification Worker",
|
||||
"Shows a notification when new streams are available.",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel(
|
||||
@ -46,9 +109,4 @@ class MyApp : Application() {
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
var seekTo: Long? = 0
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,35 @@
|
||||
package com.github.libretube.preferences
|
||||
package com.github.libretube.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.libretube.DONATE_URL
|
||||
import com.github.libretube.GITHUB_URL
|
||||
import com.github.libretube.PIPED_GITHUB_URL
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.WEBLATE_URL
|
||||
import com.github.libretube.WEBSITE_URL
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.databinding.FragmentAboutBinding
|
||||
import com.github.libretube.util.ThemeHelper.getThemeColor
|
||||
import com.github.libretube.databinding.ActivityAboutBinding
|
||||
import com.github.libretube.extensions.BaseActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
private lateinit var binding: FragmentAboutBinding
|
||||
class AboutActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityAboutBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentAboutBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
settingsActivity.changeTopBarText(getString(R.string.about))
|
||||
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.website.setOnClickListener {
|
||||
openLinkFromHref(WEBSITE_URL)
|
||||
}
|
||||
binding.website.setOnLongClickListener {
|
||||
val text = context?.getString(R.string.website_summary)!!
|
||||
val text = getString(R.string.website_summary)
|
||||
showSnackBar(text)
|
||||
true
|
||||
}
|
||||
@ -51,7 +38,16 @@ class AboutFragment : Fragment() {
|
||||
openLinkFromHref(PIPED_GITHUB_URL)
|
||||
}
|
||||
binding.piped.setOnLongClickListener {
|
||||
val text = context?.getString(R.string.piped_summary)!!
|
||||
val text = getString(R.string.piped_summary)
|
||||
showSnackBar(text)
|
||||
true
|
||||
}
|
||||
|
||||
binding.translate.setOnClickListener {
|
||||
openLinkFromHref(WEBLATE_URL)
|
||||
}
|
||||
binding.translate.setOnLongClickListener {
|
||||
val text = getString(R.string.translate_summary)
|
||||
showSnackBar(text)
|
||||
true
|
||||
}
|
||||
@ -60,7 +56,7 @@ class AboutFragment : Fragment() {
|
||||
openLinkFromHref(DONATE_URL)
|
||||
}
|
||||
binding.donate.setOnLongClickListener {
|
||||
val text = context?.getString(R.string.donate_summary)!!
|
||||
val text = getString(R.string.donate_summary)
|
||||
showSnackBar(text)
|
||||
true
|
||||
}
|
||||
@ -69,7 +65,7 @@ class AboutFragment : Fragment() {
|
||||
openLinkFromHref(GITHUB_URL)
|
||||
}
|
||||
binding.github.setOnLongClickListener {
|
||||
val text = context?.getString(R.string.contributing_summary)!!
|
||||
val text = getString(R.string.contributing_summary)
|
||||
showSnackBar(text)
|
||||
true
|
||||
}
|
||||
@ -78,7 +74,7 @@ class AboutFragment : Fragment() {
|
||||
showLicense()
|
||||
}
|
||||
binding.license.setOnLongClickListener {
|
||||
val text = context?.getString(R.string.license_summary)!!
|
||||
val text = getString(R.string.license_summary)
|
||||
showSnackBar(text)
|
||||
true
|
||||
}
|
||||
@ -94,10 +90,6 @@ class AboutFragment : Fragment() {
|
||||
val snackBar = Snackbar
|
||||
.make(binding.root, text, Snackbar.LENGTH_LONG)
|
||||
|
||||
// set snackBar color
|
||||
snackBar.setBackgroundTint(getThemeColor(requireContext(), R.attr.colorSurface))
|
||||
snackBar.setTextColor(getThemeColor(requireContext(), R.attr.colorPrimary))
|
||||
|
||||
// prevent the text from being partially hidden
|
||||
snackBar.setTextMaxLines(3)
|
||||
|
||||
@ -105,7 +97,6 @@ class AboutFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun showLicense() {
|
||||
val assets = view?.context?.assets
|
||||
val licenseString = assets
|
||||
?.open("gpl3.html")
|
||||
?.bufferedReader()
|
||||
@ -119,7 +110,7 @@ class AboutFragment : Fragment() {
|
||||
Html.fromHtml(licenseString.toString())
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setPositiveButton(getString(R.string.okay)) { _, _ -> }
|
||||
.setMessage(licenseHtml)
|
||||
.create()
|
@ -1,38 +1,24 @@
|
||||
package com.github.libretube.preferences
|
||||
package com.github.libretube.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.libretube.DISCORD_URL
|
||||
import com.github.libretube.MATRIX_URL
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.REDDIT_URL
|
||||
import com.github.libretube.TELEGRAM_URL
|
||||
import com.github.libretube.TWITTER_URL
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.databinding.FragmentCommunityBinding
|
||||
import com.github.libretube.databinding.ActivityCommunityBinding
|
||||
import com.github.libretube.extensions.BaseActivity
|
||||
|
||||
class CommunityFragment : Fragment() {
|
||||
private lateinit var binding: FragmentCommunityBinding
|
||||
class CommunityActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityCommunityBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentCommunityBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
settingsActivity.changeTopBarText(getString(R.string.community))
|
||||
binding = ActivityCommunityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.telegram.setOnClickListener {
|
||||
openLinkFromHref(TELEGRAM_URL)
|
@ -1,8 +1,7 @@
|
||||
package com.github.libretube.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@ -10,70 +9,81 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import coil.ImageLoader
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.PIPED_API_URL
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.ActivityMainBinding
|
||||
import com.github.libretube.dialogs.ErrorDialog
|
||||
import com.github.libretube.extensions.BaseActivity
|
||||
import com.github.libretube.fragments.PlayerFragment
|
||||
import com.github.libretube.models.SearchViewModel
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import com.github.libretube.services.ClosingService
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.CronetHelper
|
||||
import com.github.libretube.util.LocaleHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : BaseActivity() {
|
||||
val TAG = "MainActivity"
|
||||
|
||||
lateinit var binding: ActivityMainBinding
|
||||
|
||||
lateinit var navController: NavController
|
||||
private var startFragmentId = R.id.homeFragment
|
||||
var autoRotationEnabled = false
|
||||
|
||||
lateinit var searchView: SearchView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// set the app theme (e.g. Material You)
|
||||
ThemeHelper.updateTheme(this)
|
||||
|
||||
// set the language
|
||||
LocaleHelper.updateLanguage(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
|
||||
|
||||
// enable auto rotation if turned on
|
||||
requestedOrientation = if (autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||
|
||||
// start service that gets called on closure
|
||||
try {
|
||||
startService(Intent(this, ClosingService::class.java))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
CronetHelper.initCronet(this.applicationContext)
|
||||
ConnectionHelper.imageLoader = ImageLoader.Builder(this.applicationContext)
|
||||
.callFactory(CronetHelper.callFactory)
|
||||
.build()
|
||||
|
||||
RetrofitInstance.url =
|
||||
PreferenceHelper.getString(this, "selectInstance", PIPED_API_URL)!!
|
||||
// set auth instance
|
||||
RetrofitInstance.authUrl =
|
||||
if (PreferenceHelper.getBoolean(this, "auth_instance_toggle", false)) {
|
||||
PreferenceHelper.getString(
|
||||
this,
|
||||
"selectAuthInstance",
|
||||
PIPED_API_URL
|
||||
)!!
|
||||
} else {
|
||||
RetrofitInstance.url
|
||||
}
|
||||
// save whether the data saver mode is enabled
|
||||
Globals.DATA_SAVER_MODE_ENABLED = PreferenceHelper.getBoolean(
|
||||
PreferenceKeys.DATA_SAVER_MODE,
|
||||
false
|
||||
)
|
||||
|
||||
// show noInternet Activity if no internet available on app startup
|
||||
if (!ConnectionHelper.isNetworkAvailable(this)) {
|
||||
@ -83,6 +93,9 @@ class MainActivity : AppCompatActivity() {
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// set the action bar for the activity
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
navController = findNavController(R.id.fragment)
|
||||
binding.bottomNav.setupWithNavController(navController)
|
||||
|
||||
@ -93,12 +106,14 @@ class MainActivity : AppCompatActivity() {
|
||||
window.navigationBarColor = color
|
||||
|
||||
// hide the trending page if enabled
|
||||
val hideTrendingPage = PreferenceHelper.getBoolean(this, "hide_trending_page", false)
|
||||
val hideTrendingPage =
|
||||
PreferenceHelper.getBoolean(PreferenceKeys.HIDE_TRENDING_PAGE, false)
|
||||
if (hideTrendingPage) binding.bottomNav.menu.findItem(R.id.homeFragment).isVisible =
|
||||
false
|
||||
|
||||
// save start tab fragment id
|
||||
startFragmentId = when (PreferenceHelper.getString(this, "default_tab", "home")) {
|
||||
startFragmentId =
|
||||
when (PreferenceHelper.getString(PreferenceKeys.DEFAULT_TAB, "home")) {
|
||||
"home" -> R.id.homeFragment
|
||||
"subscriptions" -> R.id.subscriptionsFragment
|
||||
"library" -> R.id.libraryFragment
|
||||
@ -112,7 +127,7 @@ class MainActivity : AppCompatActivity() {
|
||||
navController.navigate(startFragmentId)
|
||||
|
||||
val labelVisibilityMode = when (
|
||||
PreferenceHelper.getString(this, "label_visibility", "always")
|
||||
PreferenceHelper.getString(PreferenceKeys.LABEL_VISIBILITY, "always")
|
||||
) {
|
||||
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
|
||||
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
|
||||
@ -121,10 +136,13 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
binding.bottomNav.labelVisibilityMode = labelVisibilityMode
|
||||
|
||||
binding.bottomNav.setOnApplyWindowInsetsListener(null)
|
||||
|
||||
binding.bottomNav.setOnItemSelectedListener {
|
||||
// clear backstack if it's the start fragment
|
||||
if (startFragmentId == it.itemId) navController.backQueue.clear()
|
||||
// set menu item on click listeners
|
||||
removeSearchFocus()
|
||||
when (it.itemId) {
|
||||
R.id.homeFragment -> {
|
||||
navController.navigate(R.id.homeFragment)
|
||||
@ -140,21 +158,139 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
binding.toolbar.title = ThemeHelper.getStyledAppName(this)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
// settings activity stuff
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_search -> {
|
||||
/**
|
||||
* handle error logs
|
||||
*/
|
||||
val log = PreferenceHelper.getErrorLog()
|
||||
if (log != "") ErrorDialog().show(supportFragmentManager, null)
|
||||
|
||||
setupBreakReminder()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a break reminder when watched too long
|
||||
*/
|
||||
private fun setupBreakReminder() {
|
||||
val breakReminderPref = PreferenceHelper.getString(
|
||||
PreferenceKeys.BREAK_REMINDER,
|
||||
"disabled"
|
||||
)
|
||||
if (breakReminderPref == "disabled") return
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{
|
||||
try {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.share_with_time))
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.already_spent_time,
|
||||
breakReminderPref
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.okay, null)
|
||||
.show()
|
||||
} catch (e: Exception) {
|
||||
kotlin.runCatching {
|
||||
Toast.makeText(this, R.string.take_a_break, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
breakReminderPref.toLong() * 60 * 1000
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeSearchFocus() {
|
||||
searchView.setQuery("", false)
|
||||
searchView.clearFocus()
|
||||
searchView.onActionViewCollapsed()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
menuInflater.inflate(R.menu.action_bar, menu)
|
||||
|
||||
// stuff for the search in the topBar
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
searchView = searchItem.actionView as SearchView
|
||||
|
||||
val searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
|
||||
|
||||
searchView.setOnSearchClickListener {
|
||||
if (navController.currentDestination?.id != R.id.searchResultFragment) {
|
||||
searchViewModel.setQuery(null)
|
||||
navController.navigate(R.id.searchFragment)
|
||||
}
|
||||
}
|
||||
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
val bundle = Bundle()
|
||||
bundle.putString("query", query)
|
||||
navController.navigate(R.id.searchResultFragment, bundle)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
if (navController.currentDestination?.id != R.id.searchFragment) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString("query", newText)
|
||||
navController.navigate(R.id.searchFragment, bundle)
|
||||
} else {
|
||||
searchViewModel.setQuery(newText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
|
||||
val currentFragmentId = navController.currentDestination?.id
|
||||
if (currentFragmentId == R.id.searchFragment || currentFragmentId == R.id.searchResultFragment) {
|
||||
onBackPressed()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
searchView.setOnCloseListener {
|
||||
if (navController.currentDestination?.id == R.id.searchFragment) {
|
||||
searchViewModel.setQuery(null)
|
||||
onBackPressed()
|
||||
}
|
||||
false
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
return when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
val settingsIntent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(settingsIntent)
|
||||
true
|
||||
}
|
||||
R.id.action_about -> {
|
||||
val aboutIntent = Intent(this, AboutActivity::class.java)
|
||||
startActivity(aboutIntent)
|
||||
true
|
||||
}
|
||||
R.id.action_community -> {
|
||||
val communityIntent = Intent(this, CommunityActivity::class.java)
|
||||
startActivity(communityIntent)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,92 +299,95 @@ class MainActivity : AppCompatActivity() {
|
||||
val intentData: Uri? = intent?.data
|
||||
// check whether an URI got submitted over the intent data
|
||||
if (intentData != null && intentData.host != null && intentData.path != null) {
|
||||
Log.d("intentData", "${intentData.host} ${intentData.path} ")
|
||||
Log.d(TAG, "intentData: ${intentData.host} ${intentData.path} ")
|
||||
// load the URI of the submitted link (e.g. video)
|
||||
loadIntentData(intentData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadIntentData(data: Uri) {
|
||||
// channel
|
||||
if (data.path!!.contains("/channel/") ||
|
||||
if (data.path!!.contains("/channel/")
|
||||
) {
|
||||
val channelId = data.path!!
|
||||
.replace("/channel/", "")
|
||||
|
||||
loadChannel(channelId = channelId)
|
||||
} else if (
|
||||
data.path!!.contains("/c/") ||
|
||||
data.path!!.contains("/user/")
|
||||
) {
|
||||
Log.i(TAG, "URI Type: Channel")
|
||||
var channel = data.path
|
||||
channel = channel!!.replace("/c/", "")
|
||||
channel = channel.replace("/user/", "")
|
||||
val bundle = bundleOf("channel_id" to channel)
|
||||
navController.navigate(R.id.channelFragment, bundle)
|
||||
} else if (data.path!!.contains("/playlist")) {
|
||||
Log.i(TAG, "URI Type: Playlist")
|
||||
var playlist = data.query!!
|
||||
if (playlist.contains("&")) {
|
||||
val playlists = playlist.split("&")
|
||||
for (v in playlists) {
|
||||
val channelName = data.path!!
|
||||
.replace("/c/", "")
|
||||
.replace("/user/", "")
|
||||
|
||||
loadChannel(channelName = channelName)
|
||||
} else if (
|
||||
data.path!!.contains("/playlist")
|
||||
) {
|
||||
var playlistId = data.query!!
|
||||
if (playlistId.contains("&")) {
|
||||
for (v in playlistId.split("&")) {
|
||||
if (v.contains("list=")) {
|
||||
playlist = v
|
||||
playlistId = v.replace("list=", "")
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
playlistId = playlistId.replace("list=", "")
|
||||
}
|
||||
playlist = playlist.replace("list=", "")
|
||||
val bundle = bundleOf("playlist_id" to playlist)
|
||||
navController.navigate(R.id.playlistFragment, bundle)
|
||||
} else if (data.path!!.contains("/shorts/") ||
|
||||
|
||||
loadPlaylist(playlistId)
|
||||
} else if (
|
||||
data.path!!.contains("/shorts/") ||
|
||||
data.path!!.contains("/embed/") ||
|
||||
data.path!!.contains("/v/")
|
||||
) {
|
||||
Log.i(TAG, "URI Type: Video")
|
||||
val watch = data.path!!
|
||||
val videoId = data.path!!
|
||||
.replace("/shorts/", "")
|
||||
.replace("/v/", "")
|
||||
.replace("/embed/", "")
|
||||
val bundle = Bundle()
|
||||
bundle.putString("videoId", watch)
|
||||
// for time stamped links
|
||||
if (data.query != null && data.query?.contains("t=")!!) {
|
||||
val timeStamp = data.query.toString().split("t=")[1]
|
||||
bundle.putLong("timeStamp", timeStamp.toLong())
|
||||
}
|
||||
loadWatch(bundle)
|
||||
|
||||
loadVideo(videoId, data.query)
|
||||
} else if (data.path!!.contains("/watch") && data.query != null) {
|
||||
Log.d("dafaq", data.query!!)
|
||||
var watch = data.query!!
|
||||
if (watch.contains("&")) {
|
||||
val watches = watch.split("&")
|
||||
var videoId = data.query!!
|
||||
|
||||
if (videoId.contains("&")) {
|
||||
val watches = videoId.split("&")
|
||||
for (v in watches) {
|
||||
if (v.contains("v=")) {
|
||||
watch = v
|
||||
videoId = v.replace("v=", "")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
val bundle = Bundle()
|
||||
bundle.putString("videoId", watch.replace("v=", ""))
|
||||
// for time stamped links
|
||||
if (data.query != null && data.query?.contains("t=")!!) {
|
||||
val timeStamp = data.query.toString().split("t=")[1]
|
||||
bundle.putLong("timeStamp", timeStamp.toLong())
|
||||
}
|
||||
loadWatch(bundle)
|
||||
} else {
|
||||
val watch = data.path!!.replace("/", "")
|
||||
val bundle = Bundle()
|
||||
bundle.putString("videoId", watch)
|
||||
// for time stamped links
|
||||
if (data.query != null && data.query?.contains("t=")!!) {
|
||||
val timeStamp = data.query.toString().split("t=")[1]
|
||||
bundle.putLong("timeStamp", timeStamp.toLong())
|
||||
videoId = videoId
|
||||
.replace("v=", "")
|
||||
}
|
||||
loadWatch(bundle)
|
||||
|
||||
loadVideo(videoId, data.query)
|
||||
} else {
|
||||
val videoId = data.path!!.replace("/", "")
|
||||
|
||||
loadVideo(videoId, data.query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadWatch(bundle: Bundle) {
|
||||
private fun loadVideo(videoId: String, query: String?) {
|
||||
Log.i(TAG, "URI type: Video")
|
||||
|
||||
val bundle = Bundle()
|
||||
Log.e(TAG, videoId)
|
||||
|
||||
// for time stamped links
|
||||
if (query != null && query.contains("t=")) {
|
||||
val timeStamp = query.toString().split("t=")[1]
|
||||
bundle.putLong("timeStamp", timeStamp.toLong())
|
||||
}
|
||||
|
||||
bundle.putString("videoId", videoId)
|
||||
val frag = PlayerFragment()
|
||||
frag.arguments = bundle
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.remove(PlayerFragment())
|
||||
.commit()
|
||||
@ -262,13 +401,36 @@ class MainActivity : AppCompatActivity() {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
private fun loadChannel(
|
||||
channelId: String? = null,
|
||||
channelName: String? = null
|
||||
) {
|
||||
Log.i(TAG, "Uri Type: Channel")
|
||||
|
||||
val bundle = if (channelId != null) bundleOf("channel_id" to channelId)
|
||||
else bundleOf("channel_name" to channelName)
|
||||
navController.navigate(R.id.channelFragment, bundle)
|
||||
}
|
||||
|
||||
private fun loadPlaylist(playlistId: String) {
|
||||
Log.i(TAG, "Uri Type: Playlist")
|
||||
|
||||
val bundle = bundleOf("playlist_id" to playlistId)
|
||||
navController.navigate(R.id.playlistFragment, bundle)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// remove focus from search
|
||||
removeSearchFocus()
|
||||
navController.popBackStack(R.id.searchFragment, false)
|
||||
|
||||
if (binding.mainMotionLayout.progress == 0F) {
|
||||
try {
|
||||
minimizePlayer()
|
||||
} catch (e: Exception) {
|
||||
if (navController.currentDestination?.id == startFragmentId) {
|
||||
super.onBackPressed()
|
||||
// close app
|
||||
moveTaskToBack(true)
|
||||
} else {
|
||||
navController.popBackStack()
|
||||
}
|
||||
@ -292,7 +454,9 @@ class MainActivity : AppCompatActivity() {
|
||||
enableTransition(R.id.yt_transition, true)
|
||||
}
|
||||
findViewById<LinearLayout>(R.id.linLayout).visibility = View.VISIBLE
|
||||
Globals.isFullScreen = false
|
||||
Globals.IS_FULL_SCREEN = false
|
||||
requestedOrientation = if (autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@ -330,9 +494,13 @@ class MainActivity : AppCompatActivity() {
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
)
|
||||
}
|
||||
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||
}
|
||||
|
||||
private fun unsetFullscreen() {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
||||
@ -358,12 +526,3 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.hideKeyboard() {
|
||||
view?.let { activity?.hideKeyboard(it) }
|
||||
}
|
||||
|
||||
fun Context.hideKeyboard(view: View) {
|
||||
val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
|
@ -2,18 +2,17 @@ package com.github.libretube.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.ActivityNointernetBinding
|
||||
import com.github.libretube.extensions.BaseActivity
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class NoInternetActivity : AppCompatActivity() {
|
||||
class NoInternetActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityNointernetBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.updateTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityNointernetBinding.inflate(layoutInflater)
|
||||
|
@ -5,11 +5,11 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.extensions.BaseActivity
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
|
||||
class RouterActivity : AppCompatActivity() {
|
||||
class RouterActivity : BaseActivity() {
|
||||
val TAG = "RouterActivity"
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -1,32 +1,19 @@
|
||||
package com.github.libretube.activities
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.ActivitySettingsBinding
|
||||
import com.github.libretube.extensions.BaseActivity
|
||||
import com.github.libretube.preferences.MainSettings
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
class SettingsActivity : BaseActivity() {
|
||||
val TAG = "SettingsActivity"
|
||||
lateinit var binding: ActivitySettingsBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.updateTheme(this)
|
||||
|
||||
// apply the theme for the preference dialogs
|
||||
setTheme(R.style.MaterialAlertDialog)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
overridePendingTransition(50, 50)
|
||||
}
|
||||
binding.root.alpha = 0F
|
||||
binding.root.animate().alpha(1F).duration = 300
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
@ -43,11 +30,12 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (Globals.isCurrentViewMainSettings) {
|
||||
when (supportFragmentManager.findFragmentById(R.id.settings)) {
|
||||
is MainSettings -> {
|
||||
super.onBackPressed()
|
||||
finishAndRemoveTask()
|
||||
} else {
|
||||
Globals.isCurrentViewMainSettings = true
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainSettings())
|
||||
@ -55,6 +43,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
changeTopBarText(getString(R.string.settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changeTopBarText(text: String) {
|
||||
if (this::binding.isInitialized) binding.topBarTextView.text = text
|
||||
|
@ -1,19 +1,18 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.VideoChannelRowBinding
|
||||
import com.github.libretube.databinding.VideoRowBinding
|
||||
import com.github.libretube.dialogs.VideoOptionsDialog
|
||||
import com.github.libretube.fragments.PlayerFragment
|
||||
import com.github.libretube.obj.StreamItem
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.formatShort
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.github.libretube.util.setWatchProgressLength
|
||||
import com.github.libretube.util.toID
|
||||
|
||||
class ChannelAdapter(
|
||||
private val videoFeed: MutableList<StreamItem>,
|
||||
@ -26,47 +25,39 @@ class ChannelAdapter(
|
||||
}
|
||||
|
||||
fun updateItems(newItems: List<StreamItem>) {
|
||||
val feedSize = videoFeed.size
|
||||
videoFeed.addAll(newItems)
|
||||
notifyDataSetChanged()
|
||||
notifyItemRangeInserted(feedSize, newItems.size)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding = VideoChannelRowBinding.inflate(layoutInflater, parent, false)
|
||||
val binding = VideoRowBinding.inflate(layoutInflater, parent, false)
|
||||
return ChannelViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ChannelViewHolder, position: Int) {
|
||||
val trending = videoFeed[position]
|
||||
holder.binding.apply {
|
||||
channelDescription.text = trending.title
|
||||
channelViews.text =
|
||||
videoTitle.text = trending.title
|
||||
videoInfo.text =
|
||||
trending.views.formatShort() + " • " +
|
||||
DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
|
||||
channelDuration.text =
|
||||
thumbnailDuration.text =
|
||||
DateUtils.formatElapsedTime(trending.duration!!)
|
||||
Picasso.get().load(trending.thumbnail).into(channelThumbnail)
|
||||
ConnectionHelper.loadImage(trending.thumbnail, thumbnail)
|
||||
root.setOnClickListener {
|
||||
var bundle = Bundle()
|
||||
bundle.putString("videoId", trending.url!!.replace("/watch?v=", ""))
|
||||
var frag = PlayerFragment()
|
||||
frag.arguments = bundle
|
||||
val activity = root.context as AppCompatActivity
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.remove(PlayerFragment())
|
||||
.commit()
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, frag)
|
||||
.commitNow()
|
||||
NavigationHelper.navigateVideo(root.context, trending.url)
|
||||
}
|
||||
val videoId = trending.url.toID()
|
||||
root.setOnLongClickListener {
|
||||
val videoId = trending.url!!.replace("/watch?v=", "")
|
||||
VideoOptionsDialog(videoId, root.context)
|
||||
.show(childFragmentManager, VideoOptionsDialog.TAG)
|
||||
VideoOptionsDialog(videoId)
|
||||
.show(childFragmentManager, VideoOptionsDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
watchProgress.setWatchProgressLength(videoId, trending.duration!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelViewHolder(val binding: VideoChannelRowBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
class ChannelViewHolder(val binding: VideoRowBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
@ -1,18 +1,21 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.ChapterColumnBinding
|
||||
import com.github.libretube.obj.ChapterSegment
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.squareup.picasso.Picasso
|
||||
|
||||
class ChaptersAdapter(
|
||||
private val chapters: List<ChapterSegment>,
|
||||
private val exoPlayer: ExoPlayer
|
||||
) : RecyclerView.Adapter<ChaptersViewHolder>() {
|
||||
val TAG = "ChaptersAdapter"
|
||||
private var selectedPosition = 0
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
@ -23,16 +26,30 @@ class ChaptersAdapter(
|
||||
override fun onBindViewHolder(holder: ChaptersViewHolder, position: Int) {
|
||||
val chapter = chapters[position]
|
||||
holder.binding.apply {
|
||||
Picasso.get().load(chapter.image).fit().centerCrop().into(chapterImage)
|
||||
ConnectionHelper.loadImage(chapter.image, chapterImage)
|
||||
chapterTitle.text = chapter.title
|
||||
|
||||
if (selectedPosition == position) {
|
||||
// get the color for highlighted controls
|
||||
val color =
|
||||
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
|
||||
chapterLL.setBackgroundColor(color)
|
||||
} else chapterLL.setBackgroundColor(Color.TRANSPARENT)
|
||||
root.setOnClickListener {
|
||||
updateSelectedPosition(position)
|
||||
val chapterStart = chapter.start!! * 1000 // s -> ms
|
||||
exoPlayer.seekTo(chapterStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelectedPosition(newPosition: Int) {
|
||||
val oldPosition = selectedPosition
|
||||
selectedPosition = newPosition
|
||||
notifyItemChanged(oldPosition)
|
||||
notifyItemChanged(newPosition)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return chapters.size
|
||||
}
|
||||
|
@ -5,18 +5,16 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.CommentsRowBinding
|
||||
import com.github.libretube.obj.Comment
|
||||
import com.github.libretube.obj.CommentsPage
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.formatShort
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -53,7 +51,7 @@ class CommentsAdapter(
|
||||
" • " + comment.commentedTime.toString()
|
||||
commentText.text =
|
||||
comment.commentText.toString()
|
||||
Picasso.get().load(comment.thumbnail).fit().centerCrop().into(commentorImage)
|
||||
ConnectionHelper.loadImage(comment.thumbnail, commentorImage)
|
||||
likesTextView.text =
|
||||
comment.likeCount?.toLong().formatShort()
|
||||
if (comment.verified == true) {
|
||||
@ -66,19 +64,7 @@ class CommentsAdapter(
|
||||
heartedImageView.visibility = View.VISIBLE
|
||||
}
|
||||
commentorImage.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to comment.commentorUrl)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
try {
|
||||
val mainMotionLayout =
|
||||
activity.findViewById<MotionLayout>(R.id.mainMotionLayout)
|
||||
if (mainMotionLayout.progress == 0.toFloat()) {
|
||||
mainMotionLayout.transitionToEnd()
|
||||
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
|
||||
.transitionToEnd()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
|
||||
}
|
||||
repliesRecView.layoutManager = LinearLayoutManager(root.context)
|
||||
val repliesAdapter = RepliesAdapter(CommentsPage().comments)
|
||||
|
@ -1,24 +1,23 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.PlaylistRowBinding
|
||||
import com.github.libretube.dialogs.VideoOptionsDialog
|
||||
import com.github.libretube.fragments.PlayerFragment
|
||||
import com.github.libretube.extensions.setFormattedDuration
|
||||
import com.github.libretube.obj.PlaylistId
|
||||
import com.github.libretube.obj.StreamItem
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.github.libretube.util.setWatchProgressLength
|
||||
import com.github.libretube.util.toID
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -54,45 +53,35 @@ class PlaylistAdapter(
|
||||
holder.binding.apply {
|
||||
playlistTitle.text = streamItem.title
|
||||
playlistDescription.text = streamItem.uploaderName
|
||||
playlistDuration.text = DateUtils.formatElapsedTime(streamItem.duration!!)
|
||||
Picasso.get().load(streamItem.thumbnail).into(playlistThumbnail)
|
||||
thumbnailDuration.setFormattedDuration(streamItem.duration!!)
|
||||
ConnectionHelper.loadImage(streamItem.thumbnail, playlistThumbnail)
|
||||
root.setOnClickListener {
|
||||
var bundle = Bundle()
|
||||
bundle.putString("videoId", streamItem.url!!.replace("/watch?v=", ""))
|
||||
bundle.putString("playlistId", playlistId)
|
||||
var frag = PlayerFragment()
|
||||
frag.arguments = bundle
|
||||
val activity = root.context as AppCompatActivity
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.remove(PlayerFragment())
|
||||
.commit()
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, frag)
|
||||
.commitNow()
|
||||
NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)
|
||||
}
|
||||
val videoId = streamItem.url.toID()
|
||||
root.setOnLongClickListener {
|
||||
val videoId = streamItem.url!!.replace("/watch?v=", "")
|
||||
VideoOptionsDialog(videoId, root.context)
|
||||
.show(childFragmentManager, VideoOptionsDialog.TAG)
|
||||
VideoOptionsDialog(videoId)
|
||||
.show(childFragmentManager, VideoOptionsDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
if (isOwner) {
|
||||
deletePlaylist.visibility = View.VISIBLE
|
||||
deletePlaylist.setOnClickListener {
|
||||
val token = PreferenceHelper.getToken(root.context)
|
||||
removeFromPlaylist(token, position)
|
||||
removeFromPlaylist(position)
|
||||
}
|
||||
}
|
||||
watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromPlaylist(token: String, position: Int) {
|
||||
fun run() {
|
||||
fun removeFromPlaylist(position: Int) {
|
||||
videoFeed.removeAt(position)
|
||||
activity.runOnUiThread { notifyDataSetChanged() }
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = try {
|
||||
try {
|
||||
RetrofitInstance.authApi.removeFromPlaylist(
|
||||
token,
|
||||
PreferenceHelper.getToken(),
|
||||
PlaylistId(playlistId = playlistId, index = position)
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
@ -102,22 +91,9 @@ class PlaylistAdapter(
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "HttpException, unexpected response")
|
||||
return@launch
|
||||
} finally {
|
||||
}
|
||||
try {
|
||||
if (response.message == "ok") {
|
||||
Log.d(TAG, "deleted!")
|
||||
videoFeed.removeAt(position)
|
||||
// FIXME: This needs to run on UI thread?
|
||||
activity.runOnUiThread { notifyDataSetChanged() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistViewHolder(val binding: PlaylistRowBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
@ -4,17 +4,18 @@ import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.PlaylistsRowBinding
|
||||
import com.github.libretube.dialogs.PlaylistOptionsDialog
|
||||
import com.github.libretube.obj.PlaylistId
|
||||
import com.github.libretube.obj.Playlists
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -23,6 +24,7 @@ import java.io.IOException
|
||||
|
||||
class PlaylistsAdapter(
|
||||
private val playlists: MutableList<Playlists>,
|
||||
private val childFragmentManager: FragmentManager,
|
||||
private val activity: Activity
|
||||
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
|
||||
val TAG = "PlaylistsAdapter"
|
||||
@ -45,11 +47,12 @@ class PlaylistsAdapter(
|
||||
override fun onBindViewHolder(holder: PlaylistsViewHolder, position: Int) {
|
||||
val playlist = playlists[position]
|
||||
holder.binding.apply {
|
||||
Picasso.get().load(playlist.thumbnail).into(playlistThumbnail)
|
||||
// set imageview drawable as empty playlist if imageview empty
|
||||
if (playlistThumbnail.drawable == null) {
|
||||
if (playlist.thumbnail!!.split("/").size <= 4) {
|
||||
playlistThumbnail.setImageResource(R.drawable.ic_empty_playlist)
|
||||
playlistThumbnail.setBackgroundColor(R.attr.colorSurface)
|
||||
} else {
|
||||
ConnectionHelper.loadImage(playlist.thumbnail, playlistThumbnail)
|
||||
}
|
||||
playlistTitle.text = playlist.name
|
||||
deletePlaylist.setOnClickListener {
|
||||
@ -57,27 +60,38 @@ class PlaylistsAdapter(
|
||||
builder.setTitle(R.string.deletePlaylist)
|
||||
builder.setMessage(R.string.areYouSure)
|
||||
builder.setPositiveButton(R.string.yes) { _, _ ->
|
||||
val token = PreferenceHelper.getToken(root.context)
|
||||
deletePlaylist(playlist.id!!, token, position)
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
PreferenceHelper.getToken()
|
||||
deletePlaylist(playlist.id!!, position)
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel, null)
|
||||
builder.show()
|
||||
}
|
||||
root.setOnClickListener {
|
||||
// playlists clicked
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("playlist_id" to playlist.id)
|
||||
activity.navController.navigate(R.id.playlistFragment, bundle)
|
||||
NavigationHelper.navigatePlaylist(root.context, playlist.id, true)
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
val playlistOptionsDialog = PlaylistOptionsDialog(
|
||||
playlistId = playlist.id!!,
|
||||
isOwner = true
|
||||
)
|
||||
playlistOptionsDialog.show(
|
||||
childFragmentManager,
|
||||
PlaylistOptionsDialog::class.java.name
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deletePlaylist(id: String, token: String, position: Int) {
|
||||
private fun deletePlaylist(id: String, position: Int) {
|
||||
fun run() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = try {
|
||||
RetrofitInstance.authApi.deletePlaylist(token, PlaylistId(id))
|
||||
RetrofitInstance.authApi.deletePlaylist(
|
||||
PreferenceHelper.getToken(),
|
||||
PlaylistId(id)
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
@ -88,9 +102,7 @@ class PlaylistsAdapter(
|
||||
}
|
||||
try {
|
||||
if (response.message == "ok") {
|
||||
Log.d(TAG, "deleted!")
|
||||
playlists.removeAt(position)
|
||||
// FIXME: This needs to run on UI thread?
|
||||
activity.runOnUiThread { notifyDataSetChanged() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -3,15 +3,12 @@ package com.github.libretube.adapters
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.RepliesRowBinding
|
||||
import com.github.libretube.obj.Comment
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.formatShort
|
||||
import com.squareup.picasso.Picasso
|
||||
|
||||
class RepliesAdapter(
|
||||
private val replies: MutableList<Comment>
|
||||
@ -44,7 +41,7 @@ class RepliesAdapter(
|
||||
" • " + reply.commentedTime.toString()
|
||||
commentText.text =
|
||||
reply.commentText.toString()
|
||||
Picasso.get().load(reply.thumbnail).fit().centerCrop().into(commentorImage)
|
||||
ConnectionHelper.loadImage(reply.thumbnail, commentorImage)
|
||||
likesTextView.text =
|
||||
reply.likeCount?.toLong().formatShort()
|
||||
if (reply.verified == true) {
|
||||
@ -57,19 +54,7 @@ class RepliesAdapter(
|
||||
heartedImageView.visibility = View.VISIBLE
|
||||
}
|
||||
commentorImage.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to reply.commentorUrl)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
try {
|
||||
val mainMotionLayout =
|
||||
activity.findViewById<MotionLayout>(R.id.mainMotionLayout)
|
||||
if (mainMotionLayout.progress == 0.toFloat()) {
|
||||
mainMotionLayout.transitionToEnd()
|
||||
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
|
||||
.transitionToEnd()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
NavigationHelper.navigateVideo(root.context, reply.commentorUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,27 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.ChannelSearchRowBinding
|
||||
import com.github.libretube.databinding.ChannelRowBinding
|
||||
import com.github.libretube.databinding.PlaylistSearchRowBinding
|
||||
import com.github.libretube.databinding.VideoSearchRowBinding
|
||||
import com.github.libretube.databinding.VideoRowBinding
|
||||
import com.github.libretube.dialogs.PlaylistOptionsDialog
|
||||
import com.github.libretube.dialogs.VideoOptionsDialog
|
||||
import com.github.libretube.fragments.PlayerFragment
|
||||
import com.github.libretube.extensions.setFormattedDuration
|
||||
import com.github.libretube.obj.SearchItem
|
||||
import com.github.libretube.obj.Subscribe
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.SubscriptionHelper
|
||||
import com.github.libretube.util.formatShort
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.github.libretube.util.setWatchProgressLength
|
||||
import com.github.libretube.util.toID
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
class SearchAdapter(
|
||||
private val searchItems: MutableList<SearchItem>,
|
||||
@ -49,10 +44,10 @@ class SearchAdapter(
|
||||
|
||||
return when (viewType) {
|
||||
0 -> SearchViewHolder(
|
||||
VideoSearchRowBinding.inflate(layoutInflater, parent, false)
|
||||
VideoRowBinding.inflate(layoutInflater, parent, false)
|
||||
)
|
||||
1 -> SearchViewHolder(
|
||||
ChannelSearchRowBinding.inflate(layoutInflater, parent, false)
|
||||
ChannelRowBinding.inflate(layoutInflater, parent, false)
|
||||
)
|
||||
2 -> SearchViewHolder(
|
||||
PlaylistSearchRowBinding.inflate(layoutInflater, parent, false)
|
||||
@ -82,106 +77,76 @@ class SearchAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindWatch(item: SearchItem, binding: VideoSearchRowBinding) {
|
||||
private fun bindWatch(item: SearchItem, binding: VideoRowBinding) {
|
||||
binding.apply {
|
||||
Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchThumbnail)
|
||||
if (item.duration != -1L) {
|
||||
searchThumbnailDuration.text = DateUtils.formatElapsedTime(item.duration!!)
|
||||
} else {
|
||||
searchThumbnailDuration.text = root.context.getString(R.string.live)
|
||||
searchThumbnailDuration.setBackgroundColor(R.attr.colorPrimaryDark)
|
||||
}
|
||||
Picasso.get().load(item.uploaderAvatar).fit().centerCrop().into(searchChannelImage)
|
||||
searchDescription.text = item.title
|
||||
ConnectionHelper.loadImage(item.thumbnail, thumbnail)
|
||||
thumbnailDuration.setFormattedDuration(item.duration!!)
|
||||
ConnectionHelper.loadImage(item.uploaderAvatar, channelImage)
|
||||
videoTitle.text = item.title
|
||||
val viewsString = if (item.views?.toInt() != -1) item.views.formatShort() else ""
|
||||
val uploadDate = if (item.uploadedDate != null) item.uploadedDate else ""
|
||||
searchViews.text =
|
||||
videoInfo.text =
|
||||
if (viewsString != "" && uploadDate != "") {
|
||||
"$viewsString • $uploadDate"
|
||||
} else {
|
||||
viewsString + uploadDate
|
||||
}
|
||||
searchChannelName.text = item.uploaderName
|
||||
channelName.text = item.uploaderName
|
||||
root.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
bundle.putString("videoId", item.url!!.replace("/watch?v=", ""))
|
||||
val frag = PlayerFragment()
|
||||
frag.arguments = bundle
|
||||
val activity = root.context as AppCompatActivity
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.remove(PlayerFragment())
|
||||
.commit()
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, frag)
|
||||
.commitNow()
|
||||
NavigationHelper.navigateVideo(root.context, item.url)
|
||||
}
|
||||
val videoId = item.url.toID()
|
||||
root.setOnLongClickListener {
|
||||
val videoId = item.url!!.replace("/watch?v=", "")
|
||||
VideoOptionsDialog(videoId, root.context)
|
||||
.show(childFragmentManager, VideoOptionsDialog.TAG)
|
||||
VideoOptionsDialog(videoId)
|
||||
.show(childFragmentManager, VideoOptionsDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
searchChannelImage.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to item.uploaderUrl)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
channelImage.setOnClickListener {
|
||||
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
|
||||
}
|
||||
watchProgress.setWatchProgressLength(videoId, item.duration!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindChannel(item: SearchItem, binding: ChannelSearchRowBinding) {
|
||||
private fun bindChannel(item: SearchItem, binding: ChannelRowBinding) {
|
||||
binding.apply {
|
||||
Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchChannelImage)
|
||||
ConnectionHelper.loadImage(item.thumbnail, searchChannelImage)
|
||||
searchChannelName.text = item.name
|
||||
searchViews.text = root.context.getString(
|
||||
R.string.subscribers,
|
||||
item.subscribers.formatShort()
|
||||
) + " • " + root.context.getString(R.string.videoCount, item.videos.toString())
|
||||
root.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to item.url)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
NavigationHelper.navigateChannel(root.context, item.url)
|
||||
}
|
||||
val channelId = item.url?.replace("/channel/", "")!!
|
||||
val token = PreferenceHelper.getToken(root.context)
|
||||
val channelId = item.url.toID()
|
||||
|
||||
// only show subscribe button if logged in
|
||||
if (token != "") isSubscribed(channelId, token, binding)
|
||||
isSubscribed(channelId, binding)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSubscribed(channelId: String, token: String, binding: ChannelSearchRowBinding) {
|
||||
var isSubscribed = false
|
||||
|
||||
private fun isSubscribed(channelId: String, binding: ChannelRowBinding) {
|
||||
// check whether the user subscribed to the channel
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val response = try {
|
||||
RetrofitInstance.authApi.isSubscribed(
|
||||
channelId,
|
||||
token
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
var isSubscribed = SubscriptionHelper.isSubscribed(channelId)
|
||||
|
||||
// if subscribed change text to unsubscribe
|
||||
if (response.subscribed == true) {
|
||||
isSubscribed = true
|
||||
if (isSubscribed == true) {
|
||||
binding.searchSubButton.text = binding.root.context.getString(R.string.unsubscribe)
|
||||
}
|
||||
|
||||
// make sub button visible and set the on click listeners to (un)subscribe
|
||||
if (response.subscribed != null) {
|
||||
if (isSubscribed == null) return@launch
|
||||
binding.searchSubButton.visibility = View.VISIBLE
|
||||
|
||||
binding.searchSubButton.setOnClickListener {
|
||||
if (!isSubscribed) {
|
||||
subscribe(token, channelId)
|
||||
if (isSubscribed == false) {
|
||||
SubscriptionHelper.subscribe(channelId)
|
||||
binding.searchSubButton.text =
|
||||
binding.root.context.getString(R.string.unsubscribe)
|
||||
isSubscribed = true
|
||||
} else {
|
||||
unsubscribe(token, channelId)
|
||||
SubscriptionHelper.unsubscribe(channelId)
|
||||
binding.searchSubButton.text =
|
||||
binding.root.context.getString(R.string.subscribe)
|
||||
isSubscribed = false
|
||||
@ -189,54 +154,24 @@ class SearchAdapter(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribe(token: String, channelId: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
RetrofitInstance.authApi.subscribe(
|
||||
token,
|
||||
Subscribe(channelId)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unsubscribe(token: String, channelId: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
RetrofitInstance.authApi.unsubscribe(
|
||||
token,
|
||||
Subscribe(channelId)
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPlaylist(item: SearchItem, binding: PlaylistSearchRowBinding) {
|
||||
binding.apply {
|
||||
Picasso.get().load(item.thumbnail).fit().centerCrop().into(searchThumbnail)
|
||||
ConnectionHelper.loadImage(item.thumbnail, searchThumbnail)
|
||||
if (item.videos?.toInt() != -1) searchPlaylistNumber.text = item.videos.toString()
|
||||
searchDescription.text = item.name
|
||||
searchName.text = item.uploaderName
|
||||
if (item.videos?.toInt() != -1) {
|
||||
searchPlaylistNumber.text =
|
||||
searchPlaylistVideos.text =
|
||||
root.context.getString(R.string.videoCount, item.videos.toString())
|
||||
}
|
||||
root.setOnClickListener {
|
||||
// playlist clicked
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("playlist_id" to item.url)
|
||||
activity.navController.navigate(R.id.playlistFragment, bundle)
|
||||
NavigationHelper.navigatePlaylist(root.context, item.url, false)
|
||||
}
|
||||
root.setOnLongClickListener {
|
||||
val playlistId = item.url!!.replace("/playlist?list=", "")
|
||||
PlaylistOptionsDialog(playlistId, false, root.context)
|
||||
.show(childFragmentManager, "PlaylistOptionsDialog")
|
||||
val playlistId = item.url!!.toID()
|
||||
PlaylistOptionsDialog(playlistId, false)
|
||||
.show(childFragmentManager, PlaylistOptionsDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
}
|
||||
@ -244,15 +179,15 @@ class SearchAdapter(
|
||||
}
|
||||
|
||||
class SearchViewHolder : RecyclerView.ViewHolder {
|
||||
var videoRowBinding: VideoSearchRowBinding? = null
|
||||
var channelRowBinding: ChannelSearchRowBinding? = null
|
||||
var videoRowBinding: VideoRowBinding? = null
|
||||
var channelRowBinding: ChannelRowBinding? = null
|
||||
var playlistRowBinding: PlaylistSearchRowBinding? = null
|
||||
|
||||
constructor(binding: VideoSearchRowBinding) : super(binding.root) {
|
||||
constructor(binding: VideoRowBinding) : super(binding.root) {
|
||||
videoRowBinding = binding
|
||||
}
|
||||
|
||||
constructor(binding: ChannelSearchRowBinding) : super(binding.root) {
|
||||
constructor(binding: ChannelRowBinding) : super(binding.root) {
|
||||
channelRowBinding = binding
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,15 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.SearchhistoryRowBinding
|
||||
import com.github.libretube.fragments.SearchFragment
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
|
||||
class SearchHistoryAdapter(
|
||||
private val context: Context,
|
||||
private var historyList: List<String>,
|
||||
private val editText: EditText,
|
||||
private val searchFragment: SearchFragment
|
||||
private val searchView: SearchView
|
||||
) :
|
||||
RecyclerView.Adapter<SearchHistoryViewHolder>() {
|
||||
|
||||
@ -28,19 +24,18 @@ class SearchHistoryAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SearchHistoryViewHolder, position: Int) {
|
||||
val history = historyList[position]
|
||||
val historyQuery = historyList[position]
|
||||
holder.binding.apply {
|
||||
historyText.text = history
|
||||
historyText.text = historyQuery
|
||||
|
||||
deleteHistory.setOnClickListener {
|
||||
historyList = historyList - history
|
||||
PreferenceHelper.saveHistory(context, historyList)
|
||||
historyList = historyList - historyQuery
|
||||
PreferenceHelper.removeFromSearchHistory(historyQuery)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
root.setOnClickListener {
|
||||
editText.setText(history)
|
||||
searchFragment.fetchSearch(history)
|
||||
searchView.setQuery(historyQuery, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package com.github.libretube.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.SearchsuggestionRowBinding
|
||||
import com.github.libretube.fragments.SearchFragment
|
||||
|
||||
class SearchSuggestionsAdapter(
|
||||
private var suggestionsList: List<String>,
|
||||
private var editText: EditText,
|
||||
private val searchFragment: SearchFragment
|
||||
private val searchView: SearchView
|
||||
) :
|
||||
RecyclerView.Adapter<SearchSuggestionsViewHolder>() {
|
||||
|
||||
@ -31,8 +29,7 @@ class SearchSuggestionsAdapter(
|
||||
holder.binding.apply {
|
||||
suggestionText.text = suggestion
|
||||
root.setOnClickListener {
|
||||
editText.setText(suggestion)
|
||||
searchFragment.fetchSearch(editText.text.toString())
|
||||
searchView.setQuery(suggestion, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
@ -1,30 +1,20 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
|
||||
import com.github.libretube.obj.Subscribe
|
||||
import com.github.libretube.obj.Subscription
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.SubscriptionHelper
|
||||
import com.github.libretube.util.toID
|
||||
|
||||
class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscription>) :
|
||||
RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
|
||||
val TAG = "SubChannelAdapter"
|
||||
|
||||
private var subscribed = true
|
||||
private var isLoading = false
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return subscriptions.size
|
||||
}
|
||||
@ -38,67 +28,28 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<Subscrip
|
||||
|
||||
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
|
||||
val subscription = subscriptions[position]
|
||||
var subscribed = true
|
||||
|
||||
holder.binding.apply {
|
||||
subscriptionChannelName.text = subscription.name
|
||||
Picasso.get().load(subscription.avatar).into(subscriptionChannelImage)
|
||||
ConnectionHelper.loadImage(subscription.avatar, subscriptionChannelImage)
|
||||
root.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to subscription.url)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
NavigationHelper.navigateChannel(root.context, subscription.url)
|
||||
}
|
||||
subscriptionSubscribe.setOnClickListener {
|
||||
if (!isLoading) {
|
||||
isLoading = true
|
||||
val channelId = subscription.url?.replace("/channel/", "")!!
|
||||
val channelId = subscription.url.toID()
|
||||
if (subscribed) {
|
||||
unsubscribe(root.context, channelId)
|
||||
subscriptionSubscribe.text = root.context.getString(R.string.subscribe)
|
||||
SubscriptionHelper.unsubscribe(channelId)
|
||||
subscribed = false
|
||||
} else {
|
||||
subscribe(root.context, channelId)
|
||||
subscriptionSubscribe.text =
|
||||
root.context.getString(R.string.unsubscribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribe(context: Context, channelId: String) {
|
||||
fun run() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val token = PreferenceHelper.getToken(context)
|
||||
RetrofitInstance.authApi.subscribe(
|
||||
token,
|
||||
Subscribe(channelId)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
SubscriptionHelper.subscribe(channelId)
|
||||
subscribed = true
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun unsubscribe(context: Context, channelId: String) {
|
||||
fun run() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val token = PreferenceHelper.getToken(context)
|
||||
RetrofitInstance.authApi.unsubscribe(
|
||||
token,
|
||||
Subscribe(channelId)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
subscribed = false
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,96 +1,73 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.TrendingRowBinding
|
||||
import com.github.libretube.dialogs.VideoOptionsDialog
|
||||
import com.github.libretube.fragments.PlayerFragment
|
||||
import com.github.libretube.extensions.setFormattedDuration
|
||||
import com.github.libretube.obj.StreamItem
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.formatShort
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.github.libretube.util.setWatchProgressLength
|
||||
import com.github.libretube.util.toID
|
||||
|
||||
class TrendingAdapter(
|
||||
private val videoFeed: List<StreamItem>,
|
||||
private val childFragmentManager: FragmentManager
|
||||
) : RecyclerView.Adapter<TrendingViewHolder>() {
|
||||
private val streamItems: List<StreamItem>,
|
||||
private val childFragmentManager: FragmentManager,
|
||||
private val showAllAtOne: Boolean = true
|
||||
) : RecyclerView.Adapter<SubscriptionViewHolder>() {
|
||||
private val TAG = "TrendingAdapter"
|
||||
|
||||
var index = 10
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return videoFeed.size
|
||||
return if (showAllAtOne) streamItems.size
|
||||
else if (index >= streamItems.size) streamItems.size - 1
|
||||
else index
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrendingViewHolder {
|
||||
fun updateItems() {
|
||||
index += 10
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding = TrendingRowBinding.inflate(layoutInflater, parent, false)
|
||||
return TrendingViewHolder(binding)
|
||||
return SubscriptionViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrendingViewHolder, position: Int) {
|
||||
val trending = videoFeed[position]
|
||||
override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
|
||||
val trending = streamItems[position]
|
||||
holder.binding.apply {
|
||||
textViewTitle.text = trending.title
|
||||
textViewChannel.text =
|
||||
trending.uploaderName + " • " +
|
||||
trending.views.formatShort() + " • " +
|
||||
DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
|
||||
if (trending.duration != -1L) {
|
||||
thumbnailDuration.text = DateUtils.formatElapsedTime(trending.duration!!)
|
||||
} else {
|
||||
thumbnailDuration.text = root.context.getString(R.string.live)
|
||||
thumbnailDuration.setBackgroundColor(R.attr.colorPrimaryDark)
|
||||
}
|
||||
thumbnailDuration.setFormattedDuration(trending.duration!!)
|
||||
channelImage.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to trending.uploaderUrl)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
try {
|
||||
val mainMotionLayout =
|
||||
activity.findViewById<MotionLayout>(R.id.mainMotionLayout)
|
||||
if (mainMotionLayout.progress == 0.toFloat()) {
|
||||
mainMotionLayout.transitionToEnd()
|
||||
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
|
||||
.transitionToEnd()
|
||||
NavigationHelper.navigateChannel(root.context, trending.uploaderUrl)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
if (trending.thumbnail!!.isNotEmpty()) {
|
||||
Picasso.get().load(trending.thumbnail).into(thumbnail)
|
||||
}
|
||||
if (trending.uploaderAvatar!!.isNotEmpty()) {
|
||||
Picasso.get().load(trending.uploaderAvatar).into(channelImage)
|
||||
}
|
||||
|
||||
ConnectionHelper.loadImage(trending.thumbnail, thumbnail)
|
||||
ConnectionHelper.loadImage(trending.uploaderAvatar, channelImage)
|
||||
root.setOnClickListener {
|
||||
var bundle = Bundle()
|
||||
bundle.putString("videoId", trending.url!!.replace("/watch?v=", ""))
|
||||
var frag = PlayerFragment()
|
||||
frag.arguments = bundle
|
||||
val activity = root.context as AppCompatActivity
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.remove(PlayerFragment())
|
||||
.commit()
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, frag)
|
||||
.commitNow()
|
||||
NavigationHelper.navigateVideo(root.context, trending.url)
|
||||
}
|
||||
val videoId = trending.url!!.toID()
|
||||
root.setOnLongClickListener {
|
||||
val videoId = trending.url!!.replace("/watch?v=", "")
|
||||
VideoOptionsDialog(videoId, root.context)
|
||||
.show(childFragmentManager, VideoOptionsDialog.TAG)
|
||||
VideoOptionsDialog(videoId)
|
||||
.show(childFragmentManager, VideoOptionsDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
watchProgress.setWatchProgressLength(videoId, trending.duration!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrendingViewHolder(val binding: TrendingRowBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
class SubscriptionViewHolder(val binding: TrendingRowBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
@ -1,21 +1,17 @@
|
||||
package com.github.libretube.adapters
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.WatchHistoryRowBinding
|
||||
import com.github.libretube.dialogs.VideoOptionsDialog
|
||||
import com.github.libretube.fragments.PlayerFragment
|
||||
import com.github.libretube.extensions.setFormattedDuration
|
||||
import com.github.libretube.obj.WatchHistoryItem
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.NavigationHelper
|
||||
import com.github.libretube.util.setWatchProgressLength
|
||||
|
||||
class WatchHistoryAdapter(
|
||||
private val watchHistory: MutableList<WatchHistoryItem>,
|
||||
@ -24,10 +20,10 @@ class WatchHistoryAdapter(
|
||||
RecyclerView.Adapter<WatchHistoryViewHolder>() {
|
||||
private val TAG = "WatchHistoryAdapter"
|
||||
|
||||
fun clear() {
|
||||
val size = watchHistory.size
|
||||
watchHistory.clear()
|
||||
notifyItemRangeRemoved(0, size)
|
||||
fun removeFromWatchHistory(position: Int) {
|
||||
PreferenceHelper.removeFromWatchHistory(position)
|
||||
watchHistory.removeAt(position)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
|
||||
@ -41,45 +37,29 @@ class WatchHistoryAdapter(
|
||||
holder.binding.apply {
|
||||
videoTitle.text = video.title
|
||||
channelName.text = video.uploader
|
||||
uploadDate.text = video.uploadDate
|
||||
thumbnailDuration.text = DateUtils.formatElapsedTime(video.duration?.toLong()!!)
|
||||
Picasso.get().load(video.thumbnailUrl).into(thumbnail)
|
||||
Picasso.get().load(video.uploaderAvatar).into(channelImage)
|
||||
videoInfo.text = video.uploadDate
|
||||
thumbnailDuration.setFormattedDuration(video.duration!!)
|
||||
ConnectionHelper.loadImage(video.thumbnailUrl, thumbnail)
|
||||
ConnectionHelper.loadImage(video.uploaderAvatar, channelImage)
|
||||
|
||||
channelImage.setOnClickListener {
|
||||
val activity = root.context as MainActivity
|
||||
val bundle = bundleOf("channel_id" to video.uploaderUrl)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
try {
|
||||
val mainMotionLayout =
|
||||
activity.findViewById<MotionLayout>(R.id.mainMotionLayout)
|
||||
if (mainMotionLayout.progress == 0.toFloat()) {
|
||||
mainMotionLayout.transitionToEnd()
|
||||
activity.findViewById<MotionLayout>(R.id.playerMotionLayout)
|
||||
.transitionToEnd()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
|
||||
}
|
||||
|
||||
deleteBTN.setOnClickListener {
|
||||
removeFromWatchHistory(position)
|
||||
}
|
||||
|
||||
root.setOnClickListener {
|
||||
var bundle = Bundle()
|
||||
bundle.putString("videoId", video.videoId)
|
||||
var frag = PlayerFragment()
|
||||
frag.arguments = bundle
|
||||
val activity = root.context as AppCompatActivity
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.remove(PlayerFragment())
|
||||
.commit()
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, frag)
|
||||
.commitNow()
|
||||
NavigationHelper.navigateVideo(root.context, video.videoId)
|
||||
}
|
||||
root.setOnLongClickListener {
|
||||
VideoOptionsDialog(video.videoId!!, root.context)
|
||||
.show(childFragmentManager, VideoOptionsDialog.TAG)
|
||||
VideoOptionsDialog(video.videoId!!)
|
||||
.show(childFragmentManager, VideoOptionsDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
watchProgress.setWatchProgressLength(video.videoId!!, video.duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.DialogAddtoplaylistBinding
|
||||
import com.github.libretube.obj.PlaylistId
|
||||
@ -18,7 +19,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class AddtoPlaylistDialog : DialogFragment() {
|
||||
class AddToPlaylistDialog : DialogFragment() {
|
||||
private val TAG = "AddToPlaylistDialog"
|
||||
private lateinit var binding: DialogAddtoplaylistBinding
|
||||
|
||||
@ -32,7 +33,7 @@ class AddtoPlaylistDialog : DialogFragment() {
|
||||
// Get the layout inflater
|
||||
binding = DialogAddtoplaylistBinding.inflate(layoutInflater)
|
||||
|
||||
token = PreferenceHelper.getToken(requireContext())
|
||||
token = PreferenceHelper.getToken()
|
||||
|
||||
if (token != "") fetchPlaylists()
|
||||
|
||||
@ -59,24 +60,29 @@ class AddtoPlaylistDialog : DialogFragment() {
|
||||
return@launchWhenCreated
|
||||
}
|
||||
if (response.isNotEmpty()) {
|
||||
var names = emptyList<String>().toMutableList()
|
||||
for (playlist in response) {
|
||||
names.add(playlist.name!!)
|
||||
}
|
||||
val names = response.map { it.name }
|
||||
val arrayAdapter =
|
||||
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names)
|
||||
arrayAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
binding.playlistsSpinner.adapter = arrayAdapter
|
||||
if (Globals.SELECTED_PLAYLIST_ID != null) {
|
||||
var selectionIndex = 0
|
||||
response.forEachIndexed { index, playlist ->
|
||||
if (playlist.id == Globals.SELECTED_PLAYLIST_ID) selectionIndex = index
|
||||
}
|
||||
binding.playlistsSpinner.setSelection(selectionIndex)
|
||||
}
|
||||
runOnUiThread {
|
||||
binding.addToPlaylist.setOnClickListener {
|
||||
val index = binding.playlistsSpinner.selectedItemPosition
|
||||
Globals.SELECTED_PLAYLIST_ID = response[index].id!!
|
||||
addToPlaylist(
|
||||
response[binding.playlistsSpinner.selectedItemPosition].id!!
|
||||
response[index].id!!
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ class CreatePlaylistDialog : DialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
token = PreferenceHelper.getToken(requireContext())
|
||||
token = PreferenceHelper.getToken()
|
||||
|
||||
binding.createNewPlaylist.setOnClickListener {
|
||||
// avoid creating the same playlist multiple times by spamming the button
|
||||
|
@ -41,7 +41,7 @@ class CustomInstanceDialog : DialogFragment() {
|
||||
URL(customInstance.apiUrl).toURI()
|
||||
URL(customInstance.frontendUrl).toURI()
|
||||
|
||||
PreferenceHelper.saveCustomInstance(requireContext(), customInstance)
|
||||
PreferenceHelper.saveCustomInstance(customInstance)
|
||||
activity?.recreate()
|
||||
dismiss()
|
||||
} catch (e: Exception) {
|
||||
|
@ -45,7 +45,7 @@ class DeleteAccountDialog : DialogFragment() {
|
||||
private fun deleteAccount(password: String) {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
val token = PreferenceHelper.getToken()
|
||||
|
||||
try {
|
||||
RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
|
||||
@ -57,13 +57,13 @@ class DeleteAccountDialog : DialogFragment() {
|
||||
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
|
||||
logout()
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun logout() {
|
||||
PreferenceHelper.setToken(requireContext(), "")
|
||||
PreferenceHelper.setToken("")
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,20 @@
|
||||
package com.github.libretube.dialogs
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.size
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.databinding.DialogDownloadBinding
|
||||
import com.github.libretube.obj.Streams
|
||||
import com.github.libretube.services.DownloadService
|
||||
import com.github.libretube.util.PermissionHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@ -36,48 +32,25 @@ class DownloadDialog : DialogFragment() {
|
||||
return activity?.let {
|
||||
videoId = arguments?.getString("video_id")!!
|
||||
|
||||
val mainActivity = activity as MainActivity
|
||||
val builder = MaterialAlertDialogBuilder(it)
|
||||
binding = DialogDownloadBinding.inflate(layoutInflater)
|
||||
|
||||
fetchAvailableSources()
|
||||
|
||||
// request storage permissions if not granted yet
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Log.d("myz", "" + Build.VERSION.SDK_INT)
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
mainActivity,
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.MANAGE_EXTERNAL_STORAGE
|
||||
),
|
||||
1
|
||||
) // permission request code is just an int
|
||||
}
|
||||
} else {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
mainActivity,
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
PermissionHelper.requestReadWrite(requireActivity())
|
||||
|
||||
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
|
||||
|
||||
binding.audioRadio.setOnClickListener {
|
||||
binding.videoSpinner.visibility = View.GONE
|
||||
binding.audioSpinner.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.videoRadio.setOnClickListener {
|
||||
binding.audioSpinner.visibility = View.GONE
|
||||
binding.videoSpinner.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
builder.setView(binding.root)
|
||||
builder.create()
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
@ -155,14 +128,15 @@ class DownloadDialog : DialogFragment() {
|
||||
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
|
||||
|
||||
binding.download.setOnClickListener {
|
||||
val selectedAudioUrl = audioUrl[binding.audioSpinner.selectedItemPosition]
|
||||
val selectedVideoUrl = vidUrl[binding.videoSpinner.selectedItemPosition]
|
||||
val selectedAudioUrl =
|
||||
if (binding.audioRadio.isChecked) audioUrl[binding.audioSpinner.selectedItemPosition] else ""
|
||||
val selectedVideoUrl =
|
||||
if (binding.videoRadio.isChecked) vidUrl[binding.videoSpinner.selectedItemPosition] else ""
|
||||
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
intent.putExtra("videoId", videoId)
|
||||
intent.putExtra("videoName", streams.title)
|
||||
intent.putExtra("videoUrl", selectedVideoUrl)
|
||||
intent.putExtra("audioUrl", selectedAudioUrl)
|
||||
intent.putExtra("duration", duration)
|
||||
context?.startService(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -79,11 +79,10 @@ class LoginDialog : DialogFragment() {
|
||||
Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show()
|
||||
} else if (response.token != null) {
|
||||
Toast.makeText(context, R.string.loggedIn, Toast.LENGTH_SHORT).show()
|
||||
PreferenceHelper.setToken(requireContext(), response.token!!)
|
||||
PreferenceHelper.setUsername(requireContext(), login.username!!)
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(parentFragmentManager, "RequireRestartDialog")
|
||||
PreferenceHelper.setToken(response.token!!)
|
||||
PreferenceHelper.setUsername(login.username!!)
|
||||
dialog?.dismiss()
|
||||
activity?.recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,8 +111,8 @@ class LoginDialog : DialogFragment() {
|
||||
Toast.makeText(context, response.error, Toast.LENGTH_SHORT).show()
|
||||
} else if (response.token != null) {
|
||||
Toast.makeText(context, R.string.registered, Toast.LENGTH_SHORT).show()
|
||||
PreferenceHelper.setToken(requireContext(), response.token!!)
|
||||
PreferenceHelper.setUsername(requireContext(), login.username!!)
|
||||
PreferenceHelper.setToken(response.token!!)
|
||||
PreferenceHelper.setUsername(login.username!!)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +19,13 @@ class LogoutDialog : DialogFragment() {
|
||||
val builder = MaterialAlertDialogBuilder(it)
|
||||
binding = DialogLogoutBinding.inflate(layoutInflater)
|
||||
|
||||
val user = PreferenceHelper.getUsername(requireContext())
|
||||
val user = PreferenceHelper.getUsername()
|
||||
|
||||
binding.user.text =
|
||||
binding.user.text.toString() + " (" + user + ")"
|
||||
binding.logout.setOnClickListener {
|
||||
Toast.makeText(context, R.string.loggedout, Toast.LENGTH_SHORT).show()
|
||||
PreferenceHelper.setToken(requireContext(), "")
|
||||
PreferenceHelper.setToken("")
|
||||
dialog?.dismiss()
|
||||
activity?.recreate()
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.github.libretube.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.ArrayAdapter
|
||||
@ -10,27 +9,31 @@ import androidx.fragment.app.DialogFragment
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.obj.PlaylistId
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.BackgroundHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.toID
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class PlaylistOptionsDialog(
|
||||
private val playlistId: String,
|
||||
private val isOwner: Boolean,
|
||||
context: Context
|
||||
private val isOwner: Boolean
|
||||
) : DialogFragment() {
|
||||
val TAG = "PlaylistOptionsDialog"
|
||||
|
||||
private var optionsList = listOf(
|
||||
context.getString(R.string.clonePlaylist),
|
||||
context.getString(R.string.share)
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// options for the dialog
|
||||
var optionsList = listOf(
|
||||
context?.getString(R.string.playOnBackground),
|
||||
context?.getString(R.string.clonePlaylist),
|
||||
context?.getString(R.string.share)
|
||||
)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (isOwner) {
|
||||
optionsList = optionsList +
|
||||
context?.getString(R.string.deletePlaylist)!! -
|
||||
@ -49,9 +52,22 @@ class PlaylistOptionsDialog(
|
||||
)
|
||||
) { _, which ->
|
||||
when (optionsList[which]) {
|
||||
// play the playlist in the background
|
||||
context?.getString(R.string.playOnBackground) -> {
|
||||
runBlocking {
|
||||
val playlist =
|
||||
if (isOwner) RetrofitInstance.authApi.getPlaylist(playlistId)
|
||||
else RetrofitInstance.api.getPlaylist(playlistId)
|
||||
BackgroundHelper.playOnBackground(
|
||||
context = requireContext(),
|
||||
videoId = playlist.relatedStreams!![0].url.toID(),
|
||||
playlistId = playlistId
|
||||
)
|
||||
}
|
||||
}
|
||||
// Clone the playlist to the users Piped account
|
||||
context?.getString(R.string.clonePlaylist) -> {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
val token = PreferenceHelper.getToken()
|
||||
if (token != "") {
|
||||
importPlaylist(token, playlistId)
|
||||
} else {
|
||||
@ -66,10 +82,10 @@ class PlaylistOptionsDialog(
|
||||
context?.getString(R.string.share) -> {
|
||||
val shareDialog = ShareDialog(playlistId, true)
|
||||
// using parentFragmentManager, childFragmentManager doesn't work here
|
||||
shareDialog.show(parentFragmentManager, "ShareDialog")
|
||||
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
|
||||
}
|
||||
context?.getString(R.string.deletePlaylist) -> {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
val token = PreferenceHelper.getToken()
|
||||
deletePlaylist(playlistId, token)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class RequireRestartDialog : DialogFragment() {
|
||||
activity?.recreate()
|
||||
ThemeHelper.restartMainActivity(requireContext())
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create()
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ import com.github.libretube.PIPED_FRONTEND_URL
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.YOUTUBE_FRONTEND_URL
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class ShareDialog(
|
||||
private val id: String,
|
||||
private val isPlaylist: Boolean
|
||||
private val isPlaylist: Boolean,
|
||||
private val position: Long = 0L
|
||||
) : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@ -38,7 +40,14 @@ class ShareDialog(
|
||||
else -> instanceUrl
|
||||
}
|
||||
val path = if (!isPlaylist) "/watch?v=$id" else "/playlist?list=$id"
|
||||
val url = "$host$path"
|
||||
var url = "$host$path"
|
||||
if (PreferenceHelper.getBoolean(
|
||||
PreferenceKeys.SHARE_WITH_TIME_CODE,
|
||||
true
|
||||
)
|
||||
) {
|
||||
url += "?t=$position"
|
||||
}
|
||||
|
||||
val intent = Intent()
|
||||
intent.apply {
|
||||
@ -57,13 +66,12 @@ class ShareDialog(
|
||||
// get the frontend url if it's a custom instance
|
||||
private fun getCustomInstanceFrontendUrl(): String {
|
||||
val instancePref = PreferenceHelper.getString(
|
||||
requireContext(),
|
||||
"selectInstance",
|
||||
PreferenceKeys.FETCH_INSTANCE,
|
||||
PIPED_FRONTEND_URL
|
||||
)
|
||||
|
||||
// get the api urls of the other custom instances
|
||||
val customInstances = PreferenceHelper.getCustomInstances(requireContext())
|
||||
val customInstances = PreferenceHelper.getCustomInstances()
|
||||
|
||||
// return the custom instance frontend url if available
|
||||
customInstances.forEach { instance ->
|
||||
|
@ -3,43 +3,51 @@ package com.github.libretube.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.services.UpdateService
|
||||
import com.github.libretube.update.UpdateInfo
|
||||
import com.github.libretube.util.PermissionHelper
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class UpdateAvailableDialog(
|
||||
private val versionTag: String,
|
||||
private val updateLink: String
|
||||
class UpdateDialog(
|
||||
private val updateInfo: UpdateInfo
|
||||
) : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return activity?.let {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(context?.getString(R.string.update_available, versionTag))
|
||||
.setMessage(context?.getString(R.string.update_available_text))
|
||||
.setNegativeButton(context?.getString(R.string.cancel)) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
.setTitle(context?.getString(R.string.update_available, updateInfo.name))
|
||||
.setMessage(context?.getString(R.string.update_now))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(context?.getString(R.string.okay)) { _, _ ->
|
||||
val uri = Uri.parse(updateLink)
|
||||
val downloadUrl = getDownloadUrl(updateInfo)
|
||||
Log.i("downloadUrl", downloadUrl.toString())
|
||||
if (downloadUrl != null) {
|
||||
PermissionHelper.requestReadWrite(requireActivity())
|
||||
val intent = Intent(context, UpdateService::class.java)
|
||||
intent.putExtra("downloadUrl", downloadUrl)
|
||||
context?.startService(intent)
|
||||
} else {
|
||||
val uri = Uri.parse(updateInfo.html_url)
|
||||
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
.create()
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
}
|
||||
|
||||
class NoUpdateAvailableDialog() : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return activity?.let {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(context?.getString(R.string.app_uptodate))
|
||||
.setMessage(context?.getString(R.string.no_update_available))
|
||||
.setPositiveButton(context?.getString(R.string.okay)) { _, _ -> }
|
||||
.create()
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
private fun getDownloadUrl(updateInfo: UpdateInfo): String? {
|
||||
val supportedArchitectures = Build.SUPPORTED_ABIS
|
||||
supportedArchitectures.forEach { arch ->
|
||||
updateInfo.assets?.forEach { asset ->
|
||||
if (asset.name?.contains(arch) == true) return asset.browser_download_url
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
package com.github.libretube.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.PLAYER_NOTIFICATION_ID
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.BackgroundMode
|
||||
import com.github.libretube.util.BackgroundHelper
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
/**
|
||||
@ -16,24 +19,36 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
*
|
||||
* Needs the [videoId] to load the content from the right video.
|
||||
*/
|
||||
class VideoOptionsDialog(private val videoId: String, context: Context) : DialogFragment() {
|
||||
/**
|
||||
* List that stores the different menu options. In the future could be add more options here.
|
||||
*/
|
||||
private val optionsList = listOf(
|
||||
context.getString(R.string.playOnBackground),
|
||||
context.getString(R.string.addToPlaylist),
|
||||
context.getString(R.string.share)
|
||||
)
|
||||
class VideoOptionsDialog(
|
||||
private val videoId: String
|
||||
) : DialogFragment() {
|
||||
private val TAG = "VideoOptionsDialog"
|
||||
|
||||
/**
|
||||
* Dialog that returns a [MaterialAlertDialogBuilder] showing a menu of options.
|
||||
*/
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
/**
|
||||
* List that stores the different menu options. In the future could be add more options here.
|
||||
*/
|
||||
val optionsList = mutableListOf(
|
||||
context?.getString(R.string.playOnBackground),
|
||||
context?.getString(R.string.addToPlaylist),
|
||||
context?.getString(R.string.share)
|
||||
)
|
||||
|
||||
/**
|
||||
* Check whether the player is running by observing the notification
|
||||
*/
|
||||
val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.activeNotifications.forEach {
|
||||
if (it.id == PLAYER_NOTIFICATION_ID) {
|
||||
optionsList += context?.getString(R.string.add_to_queue)
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setAdapter(
|
||||
ArrayAdapter(
|
||||
requireContext(),
|
||||
@ -41,23 +56,23 @@ class VideoOptionsDialog(private val videoId: String, context: Context) : Dialog
|
||||
optionsList
|
||||
)
|
||||
) { _, which ->
|
||||
// For now, this checks the position of the option with the position that is in the
|
||||
// list. I don't like it, but we will do like this for now.
|
||||
when (optionsList[which]) {
|
||||
// This for example will be the "Background mode" option
|
||||
// Start the background mode
|
||||
context?.getString(R.string.playOnBackground) -> {
|
||||
BackgroundMode.getInstance()
|
||||
.playOnBackgroundMode(requireContext(), videoId)
|
||||
BackgroundHelper.playOnBackground(requireContext(), videoId)
|
||||
}
|
||||
// Add Video to Playlist Dialog
|
||||
context?.getString(R.string.addToPlaylist) -> {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
val token = PreferenceHelper.getToken()
|
||||
if (token != "") {
|
||||
val newFragment = AddtoPlaylistDialog()
|
||||
val newFragment = AddToPlaylistDialog()
|
||||
val bundle = Bundle()
|
||||
bundle.putString("videoId", videoId)
|
||||
newFragment.arguments = bundle
|
||||
newFragment.show(parentFragmentManager, "AddToPlaylist")
|
||||
newFragment.show(
|
||||
parentFragmentManager,
|
||||
AddToPlaylistDialog::class.java.name
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -65,14 +80,13 @@ class VideoOptionsDialog(private val videoId: String, context: Context) : Dialog
|
||||
context?.getString(R.string.share) -> {
|
||||
val shareDialog = ShareDialog(videoId, false)
|
||||
// using parentFragmentManager is important here
|
||||
shareDialog.show(parentFragmentManager, "ShareDialog")
|
||||
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
|
||||
}
|
||||
context?.getString(R.string.add_to_queue) -> {
|
||||
Globals.playingQueue += videoId
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "VideoOptionsDialog"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
12
app/src/main/java/com/github/libretube/extensions/ToID.kt
Normal file
12
app/src/main/java/com/github/libretube/extensions/ToID.kt
Normal 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
|
||||
}
|
@ -1,39 +1,43 @@
|
||||
package com.github.libretube.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.adapters.ChannelAdapter
|
||||
import com.github.libretube.databinding.FragmentChannelBinding
|
||||
import com.github.libretube.obj.Subscribe
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.util.ConnectionHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.SubscriptionHelper
|
||||
import com.github.libretube.util.formatShort
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.github.libretube.util.toID
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class ChannelFragment : Fragment() {
|
||||
class ChannelFragment : BaseFragment() {
|
||||
private val TAG = "ChannelFragment"
|
||||
private lateinit var binding: FragmentChannelBinding
|
||||
|
||||
private var channelId: String? = null
|
||||
private var channelName: String? = null
|
||||
|
||||
var nextPage: String? = null
|
||||
private var channelAdapter: ChannelAdapter? = null
|
||||
private var isLoading = true
|
||||
private var isSubscribed: Boolean = false
|
||||
private var isSubscribed: Boolean? = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
channelId = it.getString("channel_id")
|
||||
channelId = it.getString("channel_id").toID()
|
||||
channelName = it.getString("channel_name")
|
||||
?.replace("/c/", "")
|
||||
?.replace("/user/", "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,17 +53,14 @@ class ChannelFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
channelId = channelId!!.replace("/channel/", "")
|
||||
binding.channelName.text = channelId
|
||||
binding.channelRecView.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
val refreshChannel = {
|
||||
binding.channelRefresh.isRefreshing = true
|
||||
fetchChannel()
|
||||
if (PreferenceHelper.getToken(requireContext()) != "") {
|
||||
isSubscribed()
|
||||
}
|
||||
}
|
||||
refreshChannel()
|
||||
binding.channelRefresh.setOnRefreshListener {
|
||||
refreshChannel()
|
||||
@ -74,92 +75,43 @@ class ChannelFragment : Fragment() {
|
||||
if (nextPage != null && !isLoading) {
|
||||
isLoading = true
|
||||
binding.channelRefresh.isRefreshing = true
|
||||
fetchNextPage()
|
||||
fetchChannelNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSubscribed() {
|
||||
@SuppressLint("ResourceAsColor")
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
RetrofitInstance.authApi.isSubscribed(
|
||||
channelId!!,
|
||||
token
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
return@launchWhenCreated
|
||||
}
|
||||
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
|
||||
if (isSubscribed == null) return@launchWhenCreated
|
||||
|
||||
runOnUiThread {
|
||||
if (response.subscribed == true) {
|
||||
isSubscribed = true
|
||||
if (isSubscribed == true) {
|
||||
binding.channelSubscribe.text = getString(R.string.unsubscribe)
|
||||
}
|
||||
if (response.subscribed != null) {
|
||||
binding.channelSubscribe.apply {
|
||||
setOnClickListener {
|
||||
text = if (isSubscribed) {
|
||||
unsubscribe()
|
||||
|
||||
binding.channelSubscribe.setOnClickListener {
|
||||
binding.channelSubscribe.text = if (isSubscribed == true) {
|
||||
SubscriptionHelper.unsubscribe(channelId!!)
|
||||
isSubscribed = false
|
||||
getString(R.string.subscribe)
|
||||
} else {
|
||||
subscribe()
|
||||
SubscriptionHelper.subscribe(channelId!!)
|
||||
isSubscribed = true
|
||||
getString(R.string.unsubscribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun subscribe() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
RetrofitInstance.authApi.subscribe(
|
||||
token,
|
||||
Subscribe(channelId)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
isSubscribed = true
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun unsubscribe() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
RetrofitInstance.authApi.unsubscribe(
|
||||
token,
|
||||
Subscribe(channelId)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
isSubscribed = false
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun fetchChannel() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
RetrofitInstance.api.getChannel(channelId!!)
|
||||
if (channelId != null) RetrofitInstance.api.getChannel(channelId!!)
|
||||
else RetrofitInstance.api.getChannelByName(channelName!!)
|
||||
} catch (e: IOException) {
|
||||
binding.channelRefresh.isRefreshing = false
|
||||
println(e)
|
||||
@ -194,8 +146,10 @@ class ChannelFragment : Fragment() {
|
||||
binding.channelDescription.text = response.description?.trim()
|
||||
}
|
||||
|
||||
Picasso.get().load(response.bannerUrl).into(binding.channelBanner)
|
||||
Picasso.get().load(response.avatarUrl).into(binding.channelImage)
|
||||
ConnectionHelper.loadImage(response.bannerUrl, binding.channelBanner)
|
||||
ConnectionHelper.loadImage(response.avatarUrl, binding.channelImage)
|
||||
|
||||
// recyclerview of the videos by the channel
|
||||
channelAdapter = ChannelAdapter(
|
||||
response.relatedStreams!!.toMutableList(),
|
||||
childFragmentManager
|
||||
@ -207,7 +161,7 @@ class ChannelFragment : Fragment() {
|
||||
run()
|
||||
}
|
||||
|
||||
private fun fetchNextPage() {
|
||||
private fun fetchChannelNextPage() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
@ -230,10 +184,4 @@ class ChannelFragment : Fragment() {
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
}
|
||||
|
@ -6,19 +6,20 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.adapters.TrendingAdapter
|
||||
import com.github.libretube.databinding.FragmentHomeBinding
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import com.github.libretube.util.LocaleHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
class HomeFragment : BaseFragment() {
|
||||
private val TAG = "HomeFragment"
|
||||
private lateinit var binding: FragmentHomeBinding
|
||||
private lateinit var region: String
|
||||
@ -41,12 +42,11 @@ class HomeFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val grid = PreferenceHelper.getString(
|
||||
requireContext(),
|
||||
"grid",
|
||||
PreferenceKeys.GRID_COLUMNS,
|
||||
resources.getInteger(R.integer.grid_items).toString()
|
||||
)!!
|
||||
|
||||
val regionPref = PreferenceHelper.getString(requireContext(), "region", "sys")!!
|
||||
val regionPref = PreferenceHelper.getString(PreferenceKeys.REGION, "sys")!!
|
||||
|
||||
// get the system default country if auto region selected
|
||||
region = if (regionPref == "sys") {
|
||||
@ -88,10 +88,4 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
}
|
||||
|
@ -6,21 +6,23 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.adapters.PlaylistsAdapter
|
||||
import com.github.libretube.databinding.FragmentLibraryBinding
|
||||
import com.github.libretube.dialogs.CreatePlaylistDialog
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class LibraryFragment : Fragment() {
|
||||
class LibraryFragment : BaseFragment() {
|
||||
|
||||
private val TAG = "LibraryFragment"
|
||||
lateinit var token: String
|
||||
@ -43,12 +45,12 @@ class LibraryFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.playlistRecView.layoutManager = LinearLayoutManager(view.context)
|
||||
token = PreferenceHelper.getToken(requireContext())
|
||||
binding.playlistRecView.layoutManager = LinearLayoutManager(requireContext())
|
||||
token = PreferenceHelper.getToken()
|
||||
|
||||
// hide watch history button of history disabled
|
||||
val watchHistoryEnabled =
|
||||
PreferenceHelper.getBoolean(requireContext(), "watch_history_toggle", true)
|
||||
PreferenceHelper.getBoolean(PreferenceKeys.WATCH_HISTORY_TOGGLE, true)
|
||||
if (!watchHistoryEnabled) {
|
||||
binding.showWatchHistory.visibility = View.GONE
|
||||
} else {
|
||||
@ -58,8 +60,7 @@ class LibraryFragment : Fragment() {
|
||||
}
|
||||
|
||||
if (token != "") {
|
||||
binding.boogh.visibility = View.GONE
|
||||
binding.textLike.visibility = View.GONE
|
||||
binding.loginOrRegister.visibility = View.GONE
|
||||
fetchPlaylists()
|
||||
binding.playlistRefresh.isEnabled = true
|
||||
binding.playlistRefresh.setOnRefreshListener {
|
||||
@ -67,7 +68,7 @@ class LibraryFragment : Fragment() {
|
||||
}
|
||||
binding.createPlaylist.setOnClickListener {
|
||||
val newFragment = CreatePlaylistDialog()
|
||||
newFragment.show(childFragmentManager, "Create Playlist")
|
||||
newFragment.show(childFragmentManager, CreatePlaylistDialog::class.java.name)
|
||||
}
|
||||
} else {
|
||||
binding.playlistRefresh.isEnabled = false
|
||||
@ -78,7 +79,7 @@ class LibraryFragment : Fragment() {
|
||||
override fun onResume() {
|
||||
// optimize CreatePlaylistFab bottom margin if miniPlayer active
|
||||
val layoutParams = binding.createPlaylist.layoutParams as ViewGroup.MarginLayoutParams
|
||||
layoutParams.bottomMargin = if (Globals.isMiniPlayerVisible) 180 else 64
|
||||
layoutParams.bottomMargin = if (Globals.MINI_PLAYER_VISIBLE) 180 else 64
|
||||
binding.createPlaylist.layoutParams = layoutParams
|
||||
super.onResume()
|
||||
}
|
||||
@ -102,35 +103,36 @@ class LibraryFragment : Fragment() {
|
||||
binding.playlistRefresh.isRefreshing = false
|
||||
}
|
||||
if (response.isNotEmpty()) {
|
||||
runOnUiThread {
|
||||
binding.boogh.visibility = View.GONE
|
||||
binding.textLike.visibility = View.GONE
|
||||
}
|
||||
binding.loginOrRegister.visibility = View.GONE
|
||||
|
||||
val playlistsAdapter = PlaylistsAdapter(
|
||||
response.toMutableList(),
|
||||
childFragmentManager,
|
||||
requireActivity()
|
||||
)
|
||||
|
||||
// listen for playlists to become deleted
|
||||
playlistsAdapter.registerAdapterDataObserver(object :
|
||||
RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
Log.e(TAG, playlistsAdapter.itemCount.toString())
|
||||
if (playlistsAdapter.itemCount == 0) {
|
||||
binding.loginOrRegister.visibility = View.VISIBLE
|
||||
}
|
||||
super.onChanged()
|
||||
}
|
||||
})
|
||||
|
||||
binding.playlistRecView.adapter = playlistsAdapter
|
||||
} else {
|
||||
runOnUiThread {
|
||||
binding.boogh.apply {
|
||||
visibility = View.VISIBLE
|
||||
setImageResource(R.drawable.ic_list)
|
||||
}
|
||||
binding.textLike.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = getString(R.string.emptyList)
|
||||
}
|
||||
binding.loginOrRegister.visibility = View.VISIBLE
|
||||
binding.boogh.setImageResource(R.drawable.ic_list)
|
||||
binding.textLike.text = getString(R.string.emptyList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,24 +5,27 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.adapters.PlaylistAdapter
|
||||
import com.github.libretube.databinding.FragmentPlaylistBinding
|
||||
import com.github.libretube.dialogs.PlaylistOptionsDialog
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.toID
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class PlaylistFragment : Fragment() {
|
||||
class PlaylistFragment : BaseFragment() {
|
||||
private val TAG = "PlaylistFragment"
|
||||
private lateinit var binding: FragmentPlaylistBinding
|
||||
|
||||
private var playlistId: String? = null
|
||||
var nextPage: String? = null
|
||||
private var isOwner: Boolean = false
|
||||
private var nextPage: String? = null
|
||||
private var playlistAdapter: PlaylistAdapter? = null
|
||||
private var isLoading = true
|
||||
|
||||
@ -30,6 +33,7 @@ class PlaylistFragment : Fragment() {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
playlistId = it.getString("playlist_id")
|
||||
isOwner = it.getBoolean("isOwner")
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +49,7 @@ class PlaylistFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
playlistId = playlistId!!.replace("/playlist?list=", "")
|
||||
playlistId = playlistId!!.toID()
|
||||
binding.playlistRecView.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
binding.playlistProgress.visibility = View.VISIBLE
|
||||
@ -56,7 +60,9 @@ class PlaylistFragment : Fragment() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
RetrofitInstance.api.getPlaylist(playlistId!!)
|
||||
// load locally stored playlists with the auth api
|
||||
if (isOwner) RetrofitInstance.authApi.getPlaylist(playlistId!!)
|
||||
else RetrofitInstance.api.getPlaylist(playlistId!!)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
@ -70,20 +76,18 @@ class PlaylistFragment : Fragment() {
|
||||
runOnUiThread {
|
||||
binding.playlistProgress.visibility = View.GONE
|
||||
binding.playlistName.text = response.name
|
||||
binding.playlistUploader.text = response.uploader
|
||||
binding.playlistTotVideos.text =
|
||||
binding.uploader.text = response.uploader
|
||||
binding.videoCount.text =
|
||||
getString(R.string.videoCount, response.videos.toString())
|
||||
|
||||
val user = PreferenceHelper.getUsername(requireContext())
|
||||
// check whether the user owns the playlist
|
||||
val isOwner = response.uploaderUrl == null &&
|
||||
response.uploader.equals(user, true)
|
||||
|
||||
// show playlist options
|
||||
binding.optionsMenu.setOnClickListener {
|
||||
val optionsDialog =
|
||||
PlaylistOptionsDialog(playlistId!!, isOwner, requireContext())
|
||||
optionsDialog.show(childFragmentManager, "PlaylistOptionsDialog")
|
||||
PlaylistOptionsDialog(playlistId!!, isOwner)
|
||||
optionsDialog.show(
|
||||
childFragmentManager,
|
||||
PlaylistOptionsDialog::class.java.name
|
||||
)
|
||||
}
|
||||
|
||||
playlistAdapter = PlaylistAdapter(
|
||||
@ -93,6 +97,16 @@ class PlaylistFragment : Fragment() {
|
||||
requireActivity(),
|
||||
childFragmentManager
|
||||
)
|
||||
|
||||
// listen for playlist items to become deleted
|
||||
playlistAdapter!!.registerAdapterDataObserver(object :
|
||||
RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
binding.videoCount.text =
|
||||
getString(R.string.videoCount, playlistAdapter!!.itemCount.toString())
|
||||
}
|
||||
})
|
||||
|
||||
binding.playlistRecView.adapter = playlistAdapter
|
||||
binding.playlistScrollview.viewTreeObserver
|
||||
.addOnScrollChangedListener {
|
||||
@ -104,10 +118,37 @@ class PlaylistFragment : Fragment() {
|
||||
isLoading = true
|
||||
fetchNextPage()
|
||||
}
|
||||
} else {
|
||||
// scroll view is not at bottom
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* listener for swiping to the left or right
|
||||
*/
|
||||
if (isOwner) {
|
||||
val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
|
||||
0,
|
||||
ItemTouchHelper.LEFT
|
||||
) {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
direction: Int
|
||||
) {
|
||||
val position = viewHolder.absoluteAdapterPosition
|
||||
playlistAdapter!!.removeFromPlaylist(position)
|
||||
}
|
||||
}
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
|
||||
itemTouchHelper.attachToRecyclerView(binding.playlistRecView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,7 +159,14 @@ class PlaylistFragment : Fragment() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
RetrofitInstance.api.getPlaylistNextPage(playlistId!!, nextPage!!)
|
||||
// load locally stored playlists with the auth api
|
||||
if (isOwner) RetrofitInstance.authApi.getPlaylistNextPage(
|
||||
playlistId!!,
|
||||
nextPage!!
|
||||
) else RetrofitInstance.api.getPlaylistNextPage(
|
||||
playlistId!!,
|
||||
nextPage!!
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
@ -134,10 +182,4 @@ class PlaylistFragment : Fragment() {
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,34 @@
|
||||
package com.github.libretube.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView.GONE
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.TextView.VISIBLE
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.hideKeyboard
|
||||
import com.github.libretube.adapters.SearchAdapter
|
||||
import com.github.libretube.activities.MainActivity
|
||||
import com.github.libretube.adapters.SearchHistoryAdapter
|
||||
import com.github.libretube.adapters.SearchSuggestionsAdapter
|
||||
import com.github.libretube.databinding.FragmentSearchBinding
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.models.SearchViewModel
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
class SearchFragment() : BaseFragment() {
|
||||
private val TAG = "SearchFragment"
|
||||
private lateinit var binding: FragmentSearchBinding
|
||||
private val viewModel: SearchViewModel by activityViewModels()
|
||||
|
||||
private var selectedFilter = 0
|
||||
private var apiSearchFilter = "all"
|
||||
private var nextPage: String? = null
|
||||
|
||||
private var searchAdapter: SearchAdapter? = null
|
||||
private var isLoading: Boolean = true
|
||||
private var isFetchingSearch: Boolean = false
|
||||
private var query: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
}
|
||||
query = arguments?.getString("query")
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -61,109 +43,25 @@ class SearchFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
var tempSelectedItem = 0
|
||||
binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
binding.clearSearchImageView.setOnClickListener {
|
||||
binding.autoCompleteTextView.text.clear()
|
||||
}
|
||||
|
||||
binding.filterMenuImageView.setOnClickListener {
|
||||
val filterOptions = arrayOf(
|
||||
getString(R.string.all),
|
||||
getString(R.string.videos),
|
||||
getString(R.string.channels),
|
||||
getString(R.string.playlists),
|
||||
getString(R.string.music_songs),
|
||||
getString(R.string.music_videos),
|
||||
getString(R.string.music_albums),
|
||||
getString(R.string.music_playlists)
|
||||
)
|
||||
|
||||
MaterialAlertDialogBuilder(view.context)
|
||||
.setTitle(getString(R.string.choose_filter))
|
||||
.setSingleChoiceItems(filterOptions, selectedFilter) { _, id ->
|
||||
tempSelectedItem = id
|
||||
}
|
||||
.setPositiveButton(
|
||||
getString(R.string.okay)
|
||||
) { _, _ ->
|
||||
selectedFilter = tempSelectedItem
|
||||
apiSearchFilter = when (selectedFilter) {
|
||||
0 -> "all"
|
||||
1 -> "videos"
|
||||
2 -> "channels"
|
||||
3 -> "playlists"
|
||||
4 -> "music_songs"
|
||||
5 -> "music_videos"
|
||||
6 -> "music_albums"
|
||||
7 -> "music_playlists"
|
||||
else -> "all"
|
||||
}
|
||||
fetchSearch(binding.autoCompleteTextView.text.toString())
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel), null)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
// show search history
|
||||
binding.historyRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||
showHistory()
|
||||
|
||||
binding.searchRecycler.layoutManager = GridLayoutManager(view.context, 1)
|
||||
binding.autoCompleteTextView.requestFocus()
|
||||
val imm =
|
||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.autoCompleteTextView, InputMethodManager.SHOW_IMPLICIT)
|
||||
|
||||
binding.autoCompleteTextView.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
if (s!! != "") {
|
||||
binding.searchRecycler.adapter = null
|
||||
|
||||
binding.searchRecycler.viewTreeObserver
|
||||
.addOnScrollChangedListener {
|
||||
if (!binding.searchRecycler.canScrollVertically(1)) {
|
||||
fetchNextSearchItems(binding.autoCompleteTextView.text.toString())
|
||||
}
|
||||
}
|
||||
fetchSuggestions(s.toString(), binding.autoCompleteTextView)
|
||||
// waiting for the query to change
|
||||
viewModel.searchQuery.observe(viewLifecycleOwner) {
|
||||
showData(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (s!!.isEmpty()) {
|
||||
showHistory()
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.autoCompleteTextView.setOnEditorActionListener(
|
||||
OnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
hideKeyboard()
|
||||
binding.searchRecycler.visibility = VISIBLE
|
||||
binding.historyRecycler.visibility = GONE
|
||||
fetchSearch(binding.autoCompleteTextView.text.toString())
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
)
|
||||
private fun showData(query: String?) {
|
||||
// fetch the search or history
|
||||
binding.historyEmpty.visibility = View.GONE
|
||||
binding.suggestionsRecycler.visibility = View.VISIBLE
|
||||
if (query == null || query == "") showHistory()
|
||||
else fetchSuggestions(query)
|
||||
}
|
||||
|
||||
private fun fetchSuggestions(query: String, autoTextView: EditText) {
|
||||
private fun fetchSuggestions(query: String) {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
binding.searchRecycler.visibility = GONE
|
||||
binding.historyRecycler.visibility = VISIBLE
|
||||
val response = try {
|
||||
RetrofitInstance.api.getSuggestions(query)
|
||||
} catch (e: IOException) {
|
||||
@ -174,118 +72,33 @@ class SearchFragment : Fragment() {
|
||||
Log.e(TAG, "HttpException, unexpected response")
|
||||
return@launchWhenCreated
|
||||
}
|
||||
// only load the suggestions if the input field didn't get cleared yet
|
||||
val suggestionsAdapter =
|
||||
SearchSuggestionsAdapter(response, autoTextView, this@SearchFragment)
|
||||
binding.historyRecycler.adapter = suggestionsAdapter
|
||||
}
|
||||
}
|
||||
if (!isFetchingSearch) run()
|
||||
}
|
||||
|
||||
fun fetchSearch(query: String) {
|
||||
runOnUiThread {
|
||||
binding.historyRecycler.visibility = GONE
|
||||
}
|
||||
lifecycleScope.launchWhenCreated {
|
||||
isFetchingSearch = true
|
||||
hideKeyboard()
|
||||
val response = try {
|
||||
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection $e")
|
||||
return@launchWhenCreated
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "HttpException, unexpected response")
|
||||
return@launchWhenCreated
|
||||
}
|
||||
nextPage = response.nextpage
|
||||
if (response.items!!.isNotEmpty()) {
|
||||
runOnUiThread {
|
||||
binding.searchRecycler.visibility = VISIBLE
|
||||
searchAdapter = SearchAdapter(response.items, childFragmentManager)
|
||||
binding.searchRecycler.adapter = searchAdapter
|
||||
}
|
||||
}
|
||||
addToHistory(query)
|
||||
isLoading = false
|
||||
isFetchingSearch = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNextSearchItems(query: String) {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
if (!isLoading) {
|
||||
isLoading = true
|
||||
val response = try {
|
||||
RetrofitInstance.api.getSearchResultsNextPage(
|
||||
query,
|
||||
apiSearchFilter,
|
||||
nextPage!!
|
||||
SearchSuggestionsAdapter(
|
||||
response,
|
||||
(activity as MainActivity).searchView
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
return@launchWhenCreated
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "HttpException, unexpected response," + e.response())
|
||||
return@launchWhenCreated
|
||||
}
|
||||
nextPage = response.nextpage
|
||||
searchAdapter?.updateItems(response.items!!)
|
||||
isLoading = false
|
||||
runOnUiThread {
|
||||
if (viewModel.searchQuery.value != "") {
|
||||
binding.suggestionsRecycler.adapter = suggestionsAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
hideKeyboard()
|
||||
run()
|
||||
}
|
||||
|
||||
private fun showHistory() {
|
||||
binding.searchRecycler.visibility = GONE
|
||||
val historyList = PreferenceHelper.getHistory(requireContext())
|
||||
val historyList = PreferenceHelper.getSearchHistory()
|
||||
if (historyList.isNotEmpty()) {
|
||||
binding.historyRecycler.adapter =
|
||||
binding.suggestionsRecycler.adapter =
|
||||
SearchHistoryAdapter(
|
||||
requireContext(),
|
||||
historyList,
|
||||
binding.autoCompleteTextView,
|
||||
this
|
||||
(activity as MainActivity).searchView
|
||||
)
|
||||
binding.historyRecycler.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToHistory(query: String) {
|
||||
val searchHistoryEnabled =
|
||||
PreferenceHelper.getBoolean(requireContext(), "search_history_toggle", true)
|
||||
if (searchHistoryEnabled) {
|
||||
var historyList = PreferenceHelper.getHistory(requireContext())
|
||||
|
||||
if ((historyList.isNotEmpty() && historyList.contains(query)) || query == "") {
|
||||
return
|
||||
} else {
|
||||
historyList = historyList + query
|
||||
}
|
||||
|
||||
if (historyList.size > 10) {
|
||||
historyList = historyList.takeLast(10)
|
||||
}
|
||||
|
||||
PreferenceHelper.saveHistory(requireContext(), historyList)
|
||||
binding.suggestionsRecycler.visibility = View.GONE
|
||||
binding.historyEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,30 +5,35 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.adapters.SubscriptionAdapter
|
||||
import com.github.libretube.adapters.SubscriptionChannelAdapter
|
||||
import com.github.libretube.adapters.TrendingAdapter
|
||||
import com.github.libretube.databinding.FragmentSubscriptionsBinding
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.obj.StreamItem
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import com.github.libretube.util.SubscriptionHelper
|
||||
import com.github.libretube.util.toID
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class SubscriptionsFragment : Fragment() {
|
||||
class SubscriptionsFragment : BaseFragment() {
|
||||
val TAG = "SubFragment"
|
||||
private lateinit var binding: FragmentSubscriptionsBinding
|
||||
|
||||
lateinit var token: String
|
||||
private var isLoaded = false
|
||||
private var subscriptionAdapter: SubscriptionAdapter? = null
|
||||
private var subscriptionAdapter: TrendingAdapter? = null
|
||||
private var feed: List<StreamItem> = listOf()
|
||||
private var sortOrder = "most_recent"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -47,43 +52,43 @@ class SubscriptionsFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
token = PreferenceHelper.getToken(requireContext())
|
||||
token = PreferenceHelper.getToken()
|
||||
|
||||
if (token != "") {
|
||||
binding.loginOrRegister.visibility = View.GONE
|
||||
binding.subRefresh.isEnabled = true
|
||||
|
||||
binding.subProgress.visibility = View.VISIBLE
|
||||
|
||||
val grid = PreferenceHelper.getString(
|
||||
requireContext(),
|
||||
"grid",
|
||||
PreferenceKeys.GRID_COLUMNS,
|
||||
resources.getInteger(R.integer.grid_items).toString()
|
||||
)!!
|
||||
)
|
||||
binding.subFeed.layoutManager = GridLayoutManager(view.context, grid.toInt())
|
||||
fetchFeed(binding.subFeed, binding.subProgress)
|
||||
fetchFeed()
|
||||
|
||||
binding.subRefresh.setOnRefreshListener {
|
||||
fetchChannels(binding.subChannels)
|
||||
fetchFeed(binding.subFeed, binding.subProgress)
|
||||
fetchChannels()
|
||||
fetchFeed()
|
||||
}
|
||||
|
||||
binding.sortTV.setOnClickListener {
|
||||
showSortDialog()
|
||||
}
|
||||
|
||||
binding.toggleSubs.visibility = View.VISIBLE
|
||||
var loadedSubbedChannels = false
|
||||
|
||||
binding.toggleSubs.setOnClickListener {
|
||||
binding.toggle.animate().rotationBy(180F).setDuration(100).start()
|
||||
if (!binding.subChannels.isVisible) {
|
||||
if (!binding.subChannelsContainer.isVisible) {
|
||||
if (!loadedSubbedChannels) {
|
||||
binding.subChannels.layoutManager = LinearLayoutManager(context)
|
||||
fetchChannels(binding.subChannels)
|
||||
fetchChannels()
|
||||
loadedSubbedChannels = true
|
||||
}
|
||||
binding.subChannels.visibility = View.VISIBLE
|
||||
binding.subFeed.visibility = View.GONE
|
||||
binding.subChannelsContainer.visibility = View.VISIBLE
|
||||
binding.subFeedContainer.visibility = View.GONE
|
||||
} else {
|
||||
binding.subChannels.visibility = View.GONE
|
||||
binding.subFeed.visibility = View.VISIBLE
|
||||
binding.subChannelsContainer.visibility = View.GONE
|
||||
binding.subFeedContainer.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,16 +105,30 @@ class SubscriptionsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.subRefresh.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchFeed(feedRecView: RecyclerView, progressBar: ProgressBar) {
|
||||
private fun showSortDialog() {
|
||||
val sortOptions = resources.getStringArray(R.array.sortOptions)
|
||||
val sortOptionValues = resources.getStringArray(R.array.sortOptionsValues)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.sort)
|
||||
.setItems(sortOptions) { _, index ->
|
||||
binding.sortTV.text = sortOptions[index]
|
||||
sortOrder = sortOptionValues[index]
|
||||
showFeed()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun fetchFeed() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
RetrofitInstance.authApi.getFeed(token)
|
||||
feed = try {
|
||||
if (token != "") RetrofitInstance.authApi.getFeed(token)
|
||||
else RetrofitInstance.authApi.getUnauthenticatedFeed(
|
||||
SubscriptionHelper.getFormattedLocalSubscriptions()
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.toString())
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
@ -120,35 +139,46 @@ class SubscriptionsFragment : Fragment() {
|
||||
} finally {
|
||||
binding.subRefresh.isRefreshing = false
|
||||
}
|
||||
if (response.isNotEmpty()) {
|
||||
subscriptionAdapter = SubscriptionAdapter(response, childFragmentManager)
|
||||
feedRecView.adapter = subscriptionAdapter
|
||||
subscriptionAdapter?.updateItems()
|
||||
if (feed.isNotEmpty()) {
|
||||
// save the last recent video to the prefs for the notification worker
|
||||
PreferenceHelper.setLatestVideoId(feed[0].url.toID())
|
||||
// show the feed
|
||||
showFeed()
|
||||
} else {
|
||||
runOnUiThread {
|
||||
with(binding.boogh) {
|
||||
visibility = View.VISIBLE
|
||||
setImageResource(R.drawable.ic_list)
|
||||
}
|
||||
with(binding.textLike) {
|
||||
visibility = View.VISIBLE
|
||||
text = getString(R.string.emptyList)
|
||||
}
|
||||
binding.loginOrRegister.visibility = View.VISIBLE
|
||||
binding.emptyFeed.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
progressBar.visibility = View.GONE
|
||||
binding.subProgress.visibility = View.GONE
|
||||
isLoaded = true
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun fetchChannels(channelRecView: RecyclerView) {
|
||||
private fun showFeed() {
|
||||
// sort the feed
|
||||
val sortedFeed = when (sortOrder) {
|
||||
"most_recent" -> feed
|
||||
"least_recent" -> feed.reversed()
|
||||
"most_views" -> feed.sortedBy { it.views }.reversed()
|
||||
"least_views" -> feed.sortedBy { it.views }
|
||||
"channel_name_az" -> feed.sortedBy { it.uploaderName }
|
||||
"channel_name_za" -> feed.sortedBy { it.uploaderName }.reversed()
|
||||
else -> feed
|
||||
}
|
||||
subscriptionAdapter = TrendingAdapter(sortedFeed, childFragmentManager, false)
|
||||
binding.subFeed.adapter = subscriptionAdapter
|
||||
}
|
||||
|
||||
private fun fetchChannels() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
RetrofitInstance.authApi.subscriptions(token)
|
||||
if (token != "") RetrofitInstance.authApi.subscriptions(token)
|
||||
else RetrofitInstance.authApi.unauthenticatedSubscriptions(
|
||||
SubscriptionHelper.getFormattedLocalSubscriptions()
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.toString())
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
@ -160,7 +190,8 @@ class SubscriptionsFragment : Fragment() {
|
||||
binding.subRefresh.isRefreshing = false
|
||||
}
|
||||
if (response.isNotEmpty()) {
|
||||
channelRecView.adapter = SubscriptionChannelAdapter(response.toMutableList())
|
||||
binding.subChannels.adapter =
|
||||
SubscriptionChannelAdapter(response.toMutableList())
|
||||
} else {
|
||||
Toast.makeText(context, R.string.subscribeIsEmpty, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -168,10 +199,4 @@ class SubscriptionsFragment : Fragment() {
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,15 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.adapters.WatchHistoryAdapter
|
||||
import com.github.libretube.databinding.FragmentWatchHistoryBinding
|
||||
import com.github.libretube.extensions.BaseFragment
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
|
||||
class WatchHistoryFragment : Fragment() {
|
||||
class WatchHistoryFragment : BaseFragment() {
|
||||
private val TAG = "WatchHistoryFragment"
|
||||
private lateinit var binding: FragmentWatchHistoryBinding
|
||||
|
||||
@ -26,20 +28,58 @@ class WatchHistoryFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val watchHistory = PreferenceHelper.getWatchHistory(requireContext())
|
||||
val watchHistoryAdapter = WatchHistoryAdapter(watchHistory, childFragmentManager)
|
||||
val watchHistory = PreferenceHelper.getWatchHistory()
|
||||
|
||||
if (watchHistory.isEmpty()) return
|
||||
|
||||
// reversed order
|
||||
binding.watchHistoryRecView.layoutManager = LinearLayoutManager(requireContext()).apply {
|
||||
reverseLayout = true
|
||||
stackFromEnd = true
|
||||
}
|
||||
|
||||
val watchHistoryAdapter = WatchHistoryAdapter(
|
||||
watchHistory,
|
||||
childFragmentManager
|
||||
)
|
||||
|
||||
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.clearHistory.setOnClickListener {
|
||||
PreferenceHelper.removePreference(requireContext(), "watch_history")
|
||||
watchHistoryAdapter.clear()
|
||||
}
|
||||
|
||||
// reverse order
|
||||
val linearLayoutManager = LinearLayoutManager(view.context)
|
||||
linearLayoutManager.reverseLayout = true
|
||||
linearLayoutManager.stackFromEnd = true
|
||||
|
||||
binding.watchHistoryRecView.layoutManager = linearLayoutManager
|
||||
binding.historyEmpty.visibility = View.GONE
|
||||
binding.watchHistoryRecView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package com.github.libretube.interfaces
|
||||
|
||||
interface DoubleTapInterface {
|
||||
fun onEvent(x: Float)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.github.libretube.interfaces
|
||||
|
||||
interface PlayerOptionsInterface {
|
||||
|
||||
fun onAutoplayClicked()
|
||||
|
||||
fun onCaptionClicked()
|
||||
|
||||
fun onQualityClicked()
|
||||
|
||||
fun onPlaybackSpeedClicked()
|
||||
|
||||
fun onAspectRatioClicked()
|
||||
|
||||
fun onRepeatModeClicked()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -4,9 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ChapterSegment(
|
||||
var title: String?,
|
||||
var image: String?,
|
||||
var start: Long?
|
||||
) {
|
||||
constructor() : this("", "", -1)
|
||||
}
|
||||
var title: String? = null,
|
||||
var image: String? = null,
|
||||
var start: Long? = null
|
||||
)
|
||||
|
@ -4,17 +4,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Comment(
|
||||
val author: String?,
|
||||
val commentId: String?,
|
||||
val commentText: String?,
|
||||
val commentedTime: String?,
|
||||
val commentorUrl: String?,
|
||||
val repliesPage: String?,
|
||||
val hearted: Boolean?,
|
||||
val likeCount: Int?,
|
||||
val pinned: Boolean?,
|
||||
val thumbnail: String?,
|
||||
val verified: Boolean?
|
||||
) {
|
||||
constructor() : this("", "", "", "", "", "", null, 0, null, "", null)
|
||||
}
|
||||
val author: String? = null,
|
||||
val commentId: String? = null,
|
||||
val commentText: String? = null,
|
||||
val commentedTime: String? = null,
|
||||
val commentorUrl: String? = null,
|
||||
val repliesPage: String? = null,
|
||||
val hearted: Boolean? = null,
|
||||
val likeCount: Int? = null,
|
||||
val pinned: Boolean? = null,
|
||||
val thumbnail: String? = null,
|
||||
val verified: Boolean? = null
|
||||
)
|
||||
|
@ -7,6 +7,4 @@ data class CommentsPage(
|
||||
val comments: MutableList<Comment> = arrayListOf(),
|
||||
val disabled: Boolean? = null,
|
||||
val nextpage: String? = ""
|
||||
) {
|
||||
constructor() : this(arrayListOf(), null, "")
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -4,20 +4,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class PipedStream(
|
||||
var url: String?,
|
||||
var format: String?,
|
||||
var quality: String?,
|
||||
var mimeType: String?,
|
||||
var codec: String?,
|
||||
var videoOnly: Boolean?,
|
||||
var bitrate: Int?,
|
||||
var initStart: Int?,
|
||||
var initEnd: Int?,
|
||||
var indexStart: Int?,
|
||||
var indexEnd: Int?,
|
||||
var width: Int?,
|
||||
var height: Int?,
|
||||
var fps: Int?
|
||||
) {
|
||||
constructor() : this("", "", "", "", "", null, -1, -1, -1, -1, -1, -1, -1, -1)
|
||||
}
|
||||
var url: String? = null,
|
||||
var format: String? = null,
|
||||
var quality: String? = null,
|
||||
var mimeType: String? = null,
|
||||
var codec: String? = null,
|
||||
var videoOnly: Boolean? = null,
|
||||
var bitrate: Int? = null,
|
||||
var initStart: Int? = null,
|
||||
var initEnd: Int? = null,
|
||||
var indexStart: Int? = null,
|
||||
var indexEnd: Int? = null,
|
||||
var width: Int? = null,
|
||||
var height: Int? = null,
|
||||
var fps: Int? = null
|
||||
)
|
||||
|
@ -4,25 +4,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class SearchItem(
|
||||
var url: String?,
|
||||
var thumbnail: String?,
|
||||
var uploaderName: String?,
|
||||
var uploaded: Long?,
|
||||
var shortDescription: String?,
|
||||
var url: String? = null,
|
||||
var thumbnail: String? = null,
|
||||
var uploaderName: String? = null,
|
||||
var uploaded: Long? = null,
|
||||
var shortDescription: String? = null,
|
||||
// Video only attributes
|
||||
var title: String?,
|
||||
var uploaderUrl: String?,
|
||||
var uploaderAvatar: String?,
|
||||
var uploadedDate: String?,
|
||||
var duration: Long?,
|
||||
var views: Long?,
|
||||
var uploaderVerified: Boolean?,
|
||||
var title: String? = null,
|
||||
var uploaderUrl: String? = null,
|
||||
var uploaderAvatar: String? = null,
|
||||
var uploadedDate: String? = null,
|
||||
var duration: Long? = null,
|
||||
var views: Long? = null,
|
||||
var uploaderVerified: Boolean? = null,
|
||||
// Channel and Playlist attributes
|
||||
var name: String? = null,
|
||||
var description: String? = null,
|
||||
var subscribers: Long? = -1,
|
||||
var videos: Long? = -1,
|
||||
var verified: Boolean? = null
|
||||
) {
|
||||
constructor() : this("", "", "", 0, "", "", "", "", "", 0, 0, null)
|
||||
}
|
||||
)
|
||||
|
@ -4,9 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Segment(
|
||||
val actionType: String?,
|
||||
val category: String?,
|
||||
val segment: List<Float>?
|
||||
) {
|
||||
constructor() : this("", "", arrayListOf())
|
||||
}
|
||||
val actionType: String? = null,
|
||||
val category: String? = null,
|
||||
val segment: List<Float>? = arrayListOf()
|
||||
)
|
||||
|
@ -5,6 +5,4 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Segments(
|
||||
val segments: MutableList<Segment> = arrayListOf()
|
||||
) {
|
||||
constructor() : this(arrayListOf())
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -4,18 +4,16 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class StreamItem(
|
||||
var url: String?,
|
||||
var title: String?,
|
||||
var thumbnail: String?,
|
||||
var uploaderName: String?,
|
||||
var uploaderUrl: String?,
|
||||
var uploaderAvatar: String?,
|
||||
var uploadedDate: String?,
|
||||
var duration: Long?,
|
||||
var views: Long?,
|
||||
var uploaderVerified: Boolean?,
|
||||
var uploaded: Long?,
|
||||
var shortDescription: String?
|
||||
) {
|
||||
constructor() : this("", "", "", "", "", "", "", 0, 0, null, 0, "")
|
||||
}
|
||||
var url: String? = null,
|
||||
var title: String? = null,
|
||||
var thumbnail: String? = null,
|
||||
var uploaderName: String? = null,
|
||||
var uploaderUrl: String? = null,
|
||||
var uploaderAvatar: String? = null,
|
||||
var uploadedDate: String? = null,
|
||||
var duration: Long? = null,
|
||||
var views: Long? = null,
|
||||
var uploaderVerified: Boolean? = null,
|
||||
var uploaded: Long? = null,
|
||||
var shortDescription: String? = null
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ data class Streams(
|
||||
val dash: String?,
|
||||
val lbryId: String?,
|
||||
val uploaderVerified: Boolean?,
|
||||
val duration: Int?,
|
||||
val duration: Long?,
|
||||
val views: Long?,
|
||||
val likes: Long?,
|
||||
val dislikes: Long?,
|
||||
|
@ -4,11 +4,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Subtitle(
|
||||
val url: String?,
|
||||
val mimeType: String?,
|
||||
val name: String?,
|
||||
val code: String?,
|
||||
val autoGenerated: Boolean?
|
||||
) {
|
||||
constructor() : this("", "", "", "", null)
|
||||
}
|
||||
val url: String? = null,
|
||||
val mimeType: String? = null,
|
||||
val name: String? = null,
|
||||
val code: String? = null,
|
||||
val autoGenerated: Boolean? = null
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -1,12 +1,12 @@
|
||||
package com.github.libretube.obj
|
||||
|
||||
data class WatchHistoryItem(
|
||||
val videoId: String?,
|
||||
val title: String?,
|
||||
val uploadDate: String?,
|
||||
val uploader: String?,
|
||||
val uploaderUrl: String?,
|
||||
val uploaderAvatar: String?,
|
||||
val thumbnailUrl: String?,
|
||||
val duration: Int?
|
||||
val videoId: String? = null,
|
||||
val title: String? = null,
|
||||
val uploadDate: String? = null,
|
||||
val uploader: String? = null,
|
||||
val uploaderUrl: String? = null,
|
||||
val uploaderAvatar: String? = null,
|
||||
val thumbnailUrl: String? = null,
|
||||
val duration: Long? = null
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.github.libretube.obj
|
||||
|
||||
data class WatchPosition(
|
||||
val videoId: String,
|
||||
val position: Long
|
||||
val videoId: String = "",
|
||||
val position: Long = 0L
|
||||
)
|
||||
|
@ -2,13 +2,12 @@ package com.github.libretube.preferences
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.dialogs.RequireRestartDialog
|
||||
import com.github.libretube.views.MaterialPreferenceFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class AdvancedSettings : PreferenceFragmentCompat() {
|
||||
class AdvancedSettings : MaterialPreferenceFragment() {
|
||||
val TAG = "AdvancedSettings"
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@ -17,22 +16,7 @@ class AdvancedSettings : PreferenceFragmentCompat() {
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
settingsActivity.changeTopBarText(getString(R.string.advanced))
|
||||
|
||||
// clear search history
|
||||
val clearHistory = findPreference<Preference>("clear_history")
|
||||
clearHistory?.setOnPreferenceClickListener {
|
||||
PreferenceHelper.removePreference(requireContext(), "search_history")
|
||||
true
|
||||
}
|
||||
|
||||
// clear watch history and positions
|
||||
val clearWatchHistory = findPreference<Preference>("clear_watch_history")
|
||||
clearWatchHistory?.setOnPreferenceClickListener {
|
||||
PreferenceHelper.removePreference(requireContext(), "watch_history")
|
||||
PreferenceHelper.removePreference(requireContext(), "watch_positions")
|
||||
true
|
||||
}
|
||||
|
||||
val resetSettings = findPreference<Preference>("reset_settings")
|
||||
val resetSettings = findPreference<Preference>(PreferenceKeys.RESET_SETTINGS)
|
||||
resetSettings?.setOnPreferenceClickListener {
|
||||
showResetDialog()
|
||||
true
|
||||
@ -41,19 +25,18 @@ class AdvancedSettings : PreferenceFragmentCompat() {
|
||||
|
||||
private fun showResetDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setPositiveButton(R.string.reset) { _, _ ->
|
||||
// clear default preferences
|
||||
PreferenceHelper.clearPreferences(requireContext())
|
||||
|
||||
// clear login token
|
||||
PreferenceHelper.setToken(requireContext(), "")
|
||||
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
|
||||
.setTitle(R.string.reset)
|
||||
.setMessage(R.string.reset_message)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.reset) { _, _ ->
|
||||
// clear default preferences
|
||||
PreferenceHelper.clearPreferences()
|
||||
|
||||
// clear login token
|
||||
PreferenceHelper.setToken("")
|
||||
|
||||
activity?.recreate()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,21 @@
|
||||
package com.github.libretube.preferences
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.dialogs.RequireRestartDialog
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
import com.github.libretube.views.MaterialPreferenceFragment
|
||||
import com.google.android.material.color.DynamicColors
|
||||
|
||||
class AppearanceSettings : PreferenceFragmentCompat() {
|
||||
class AppearanceSettings : MaterialPreferenceFragment() {
|
||||
private val TAG = "AppearanceSettings"
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.appearance_settings, rootKey)
|
||||
@ -18,52 +23,59 @@ class AppearanceSettings : PreferenceFragmentCompat() {
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
settingsActivity.changeTopBarText(getString(R.string.appearance))
|
||||
|
||||
val themeToggle = findPreference<ListPreference>("theme_toggle")
|
||||
val themeToggle = findPreference<ListPreference>(PreferenceKeys.THEME_MODE)
|
||||
themeToggle?.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
val pureTheme = findPreference<SwitchPreferenceCompat>("pure_theme")
|
||||
val pureTheme = findPreference<SwitchPreferenceCompat>(PreferenceKeys.PURE_THEME)
|
||||
pureTheme?.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
val accentColor = findPreference<ListPreference>("accent_color")
|
||||
val accentColor = findPreference<ListPreference>(PreferenceKeys.ACCENT_COLOR)
|
||||
updateAccentColorValues(accentColor!!)
|
||||
accentColor.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
val iconChange = findPreference<ListPreference>("icon_change")
|
||||
val iconChange = findPreference<ListPreference>(PreferenceKeys.APP_ICON)
|
||||
iconChange?.setOnPreferenceChangeListener { _, newValue ->
|
||||
ThemeHelper.changeIcon(requireContext(), newValue.toString())
|
||||
true
|
||||
}
|
||||
|
||||
val gridColumns = findPreference<ListPreference>("grid")
|
||||
gridColumns?.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
true
|
||||
}
|
||||
|
||||
val hideTrending = findPreference<SwitchPreferenceCompat>("hide_trending_page")
|
||||
hideTrending?.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
true
|
||||
}
|
||||
|
||||
val labelVisibilityMode = findPreference<ListPreference>("label_visibility")
|
||||
val labelVisibilityMode = findPreference<ListPreference>(PreferenceKeys.LABEL_VISIBILITY)
|
||||
labelVisibilityMode?.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
restartDialog.show(childFragmentManager, RequireRestartDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
val systemCaptionStyle =
|
||||
findPreference<SwitchPreferenceCompat>(PreferenceKeys.SYSTEM_CAPTION_STYLE)
|
||||
val captionSettings = findPreference<Preference>(PreferenceKeys.CAPTION_SETTINGS)
|
||||
|
||||
captionSettings?.isVisible =
|
||||
PreferenceHelper.getBoolean(PreferenceKeys.SYSTEM_CAPTION_STYLE, true)
|
||||
systemCaptionStyle?.setOnPreferenceChangeListener { _, newValue ->
|
||||
captionSettings?.isVisible = newValue as Boolean
|
||||
true
|
||||
}
|
||||
|
||||
captionSettings?.setOnPreferenceClickListener {
|
||||
try {
|
||||
val captionSettingsIntent = Intent(Settings.ACTION_CAPTIONING_SETTINGS)
|
||||
startActivity(captionSettingsIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,23 +1,15 @@
|
||||
package com.github.libretube.preferences
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
@ -25,87 +17,33 @@ import com.github.libretube.dialogs.CustomInstanceDialog
|
||||
import com.github.libretube.dialogs.DeleteAccountDialog
|
||||
import com.github.libretube.dialogs.LoginDialog
|
||||
import com.github.libretube.dialogs.LogoutDialog
|
||||
import com.github.libretube.dialogs.RequireRestartDialog
|
||||
import com.github.libretube.util.ImportHelper
|
||||
import com.github.libretube.util.PermissionHelper
|
||||
import com.github.libretube.util.RetrofitInstance
|
||||
import org.json.JSONObject
|
||||
import org.json.JSONTokener
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import com.github.libretube.views.MaterialPreferenceFragment
|
||||
|
||||
class InstanceSettings : PreferenceFragmentCompat() {
|
||||
class InstanceSettings : MaterialPreferenceFragment() {
|
||||
val TAG = "InstanceSettings"
|
||||
|
||||
/**
|
||||
* result listeners for importing and exporting subscriptions
|
||||
*/
|
||||
private lateinit var getContent: ActivityResultLauncher<String>
|
||||
private lateinit var createFile: ActivityResultLauncher<String>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
MainSettings.getContent =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
// Open a specific media item using ParcelFileDescriptor.
|
||||
val resolver: ContentResolver =
|
||||
requireActivity()
|
||||
.contentResolver
|
||||
|
||||
// "rw" for read-and-write;
|
||||
// "rwt" for truncating or overwriting existing file contents.
|
||||
// val readOnlyMode = "r"
|
||||
// uri - I have got from onActivityResult
|
||||
val type = resolver.getType(uri)
|
||||
|
||||
var inputStream: InputStream? = resolver.openInputStream(uri)
|
||||
val channels = ArrayList<String>()
|
||||
if (type == "application/json") {
|
||||
val json = inputStream?.bufferedReader()?.readLines()?.get(0)
|
||||
val jsonObject = JSONTokener(json).nextValue() as JSONObject
|
||||
Log.e(TAG, jsonObject.getJSONArray("subscriptions").toString())
|
||||
for (
|
||||
i in 0 until jsonObject.getJSONArray("subscriptions")
|
||||
.length()
|
||||
) {
|
||||
var url =
|
||||
jsonObject.getJSONArray("subscriptions").getJSONObject(i)
|
||||
.getString("url")
|
||||
url = url.replace("https://www.youtube.com/channel/", "")
|
||||
Log.e(TAG, url)
|
||||
channels.add(url)
|
||||
}
|
||||
} else {
|
||||
if (type == "application/zip") {
|
||||
val zis = ZipInputStream(inputStream)
|
||||
var entry: ZipEntry? = zis.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.name.endsWith(".csv")) {
|
||||
inputStream = zis
|
||||
break
|
||||
}
|
||||
entry = zis.nextEntry
|
||||
getContent =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
ImportHelper(requireActivity()).importSubscriptions(uri)
|
||||
}
|
||||
createFile = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument()
|
||||
) { uri: Uri? ->
|
||||
ImportHelper(requireActivity()).exportSubscriptions(uri)
|
||||
}
|
||||
|
||||
inputStream?.bufferedReader()?.readLines()?.forEach {
|
||||
if (it.isNotBlank()) {
|
||||
val channelId = it.substringBefore(",")
|
||||
if (channelId.length == 24) {
|
||||
channels.add(channelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
inputStream?.close()
|
||||
|
||||
subscribe(channels)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
@ -115,25 +53,24 @@ class InstanceSettings : PreferenceFragmentCompat() {
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
settingsActivity.changeTopBarText(getString(R.string.instance))
|
||||
|
||||
val instance = findPreference<ListPreference>("selectInstance")
|
||||
val instance = findPreference<ListPreference>(PreferenceKeys.FETCH_INSTANCE)
|
||||
// fetchInstance()
|
||||
initCustomInstances(instance!!)
|
||||
instance.setOnPreferenceChangeListener { _, newValue ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
RetrofitInstance.url = newValue.toString()
|
||||
if (!PreferenceHelper.getBoolean(requireContext(), "auth_instance_toggle", false)) {
|
||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
|
||||
RetrofitInstance.authUrl = newValue.toString()
|
||||
logout()
|
||||
}
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
activity?.recreate()
|
||||
true
|
||||
}
|
||||
|
||||
val authInstance = findPreference<ListPreference>("selectAuthInstance")
|
||||
val authInstance = findPreference<ListPreference>(PreferenceKeys.AUTH_INSTANCE)
|
||||
initCustomInstances(authInstance!!)
|
||||
// hide auth instance if option deselected
|
||||
if (!PreferenceHelper.getBoolean(requireContext(), "auth_instance_toggle", false)) {
|
||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.AUTH_INSTANCE_TOGGLE, false)) {
|
||||
authInstance.isVisible = false
|
||||
}
|
||||
authInstance.setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -141,226 +78,132 @@ class InstanceSettings : PreferenceFragmentCompat() {
|
||||
RetrofitInstance.authUrl = newValue.toString()
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
logout()
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
activity?.recreate()
|
||||
true
|
||||
}
|
||||
|
||||
val authInstanceToggle = findPreference<SwitchPreferenceCompat>("auth_instance_toggle")
|
||||
val authInstanceToggle =
|
||||
findPreference<SwitchPreferenceCompat>(PreferenceKeys.AUTH_INSTANCE_TOGGLE)
|
||||
authInstanceToggle?.setOnPreferenceChangeListener { _, newValue ->
|
||||
authInstance.isVisible = newValue == true
|
||||
logout()
|
||||
// either use new auth url or the normal api url if auth instance disabled
|
||||
RetrofitInstance.authUrl = if (newValue == false) RetrofitInstance.url
|
||||
else authInstance.value
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
activity?.recreate()
|
||||
true
|
||||
}
|
||||
|
||||
val customInstance = findPreference<Preference>("customInstance")
|
||||
val customInstance = findPreference<Preference>(PreferenceKeys.CUSTOM_INSTANCE)
|
||||
customInstance?.setOnPreferenceClickListener {
|
||||
val newFragment = CustomInstanceDialog()
|
||||
newFragment.show(childFragmentManager, "CustomInstanceDialog")
|
||||
newFragment.show(childFragmentManager, CustomInstanceDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
val clearCustomInstances = findPreference<Preference>("clearCustomInstances")
|
||||
val clearCustomInstances = findPreference<Preference>(PreferenceKeys.CLEAR_CUSTOM_INSTANCES)
|
||||
clearCustomInstances?.setOnPreferenceClickListener {
|
||||
PreferenceHelper.removePreference(requireContext(), "customInstances")
|
||||
PreferenceHelper.removePreference("customInstances")
|
||||
val intent = Intent(context, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
|
||||
val login = findPreference<Preference>("login_register")
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
val login = findPreference<Preference>(PreferenceKeys.LOGIN_REGISTER)
|
||||
val token = PreferenceHelper.getToken()
|
||||
if (token != "") login?.setTitle(R.string.logout)
|
||||
login?.setOnPreferenceClickListener {
|
||||
if (token == "") {
|
||||
val newFragment = LoginDialog()
|
||||
newFragment.show(childFragmentManager, "Login")
|
||||
newFragment.show(childFragmentManager, LoginDialog::class.java.name)
|
||||
} else {
|
||||
val newFragment = LogoutDialog()
|
||||
newFragment.show(childFragmentManager, "Logout")
|
||||
newFragment.show(childFragmentManager, LogoutDialog::class.java.name)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
val deleteAccount = findPreference<Preference>("delete_account")
|
||||
val deleteAccount = findPreference<Preference>(PreferenceKeys.DELETE_ACCOUNT)
|
||||
deleteAccount?.setOnPreferenceClickListener {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
val token = PreferenceHelper.getToken()
|
||||
if (token != "") {
|
||||
val newFragment = DeleteAccountDialog()
|
||||
newFragment.show(childFragmentManager, "DeleteAccountDialog")
|
||||
newFragment.show(childFragmentManager, DeleteAccountDialog::class.java.name)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
val importFromYt = findPreference<Preference>("import_from_yt")
|
||||
importFromYt?.setOnPreferenceClickListener {
|
||||
importSubscriptions()
|
||||
val importSubscriptions = findPreference<Preference>(PreferenceKeys.IMPORT_SUBS)
|
||||
importSubscriptions?.setOnPreferenceClickListener {
|
||||
// check StorageAccess
|
||||
val accessGranted =
|
||||
PermissionHelper.isStoragePermissionGranted(requireActivity())
|
||||
// import subscriptions
|
||||
if (accessGranted) getContent.launch("*/*")
|
||||
// request permissions if not granted
|
||||
else PermissionHelper.requestReadWrite(requireActivity())
|
||||
true
|
||||
}
|
||||
|
||||
val exportSubscriptions = findPreference<Preference>(PreferenceKeys.EXPORT_SUBS)
|
||||
exportSubscriptions?.setOnPreferenceClickListener {
|
||||
createFile.launch("subscriptions.json")
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCustomInstances(instancePref: ListPreference) {
|
||||
val customInstances = PreferenceHelper.getCustomInstances(requireContext())
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val customInstances = PreferenceHelper.getCustomInstances()
|
||||
|
||||
val instanceNames = arrayListOf<String>()
|
||||
val instanceValues = arrayListOf<String>()
|
||||
|
||||
// fetch official public instances
|
||||
|
||||
val response = try {
|
||||
RetrofitInstance.api.getInstances("https://instances.tokhmi.xyz/")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
|
||||
response.forEach {
|
||||
if (it.name != null && it.api_url != null) {
|
||||
instanceNames += it.name!!
|
||||
instanceValues += it.api_url!!
|
||||
}
|
||||
}
|
||||
|
||||
var instanceNames = resources.getStringArray(R.array.instances)
|
||||
var instanceValues = resources.getStringArray(R.array.instancesValue)
|
||||
customInstances.forEach { instance ->
|
||||
instanceNames += instance.name
|
||||
instanceValues += instance.apiUrl
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
// add custom instances to the list preference
|
||||
instancePref.entries = instanceNames
|
||||
instancePref.entryValues = instanceValues
|
||||
instancePref.entries = instanceNames.toTypedArray()
|
||||
instancePref.entryValues = instanceValues.toTypedArray()
|
||||
instancePref.summaryProvider =
|
||||
Preference.SummaryProvider<ListPreference> { preference ->
|
||||
val text = preference.entry
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
"kavin.rocks (Official)"
|
||||
} else {
|
||||
text
|
||||
preference.entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logout() {
|
||||
PreferenceHelper.setToken(requireContext(), "")
|
||||
PreferenceHelper.setToken("")
|
||||
Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun fetchInstance() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
RetrofitInstance.api.getInstances("https://instances.tokhmi.xyz/")
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e("settings", "IOException, you might not have internet connection")
|
||||
return@launchWhenCreated
|
||||
} catch (e: HttpException) {
|
||||
Log.e("settings", "HttpException, unexpected response $e")
|
||||
return@launchWhenCreated
|
||||
} catch (e: Exception) {
|
||||
Log.e("settings", e.toString())
|
||||
return@launchWhenCreated
|
||||
}
|
||||
val listEntries: MutableList<String> = ArrayList()
|
||||
val listEntryValues: MutableList<String> = ArrayList()
|
||||
for (item in response) {
|
||||
listEntries.add(item.name!!)
|
||||
listEntryValues.add(item.api_url!!)
|
||||
}
|
||||
|
||||
// add custom instances to the list
|
||||
|
||||
val entries = listEntries.toTypedArray<CharSequence>()
|
||||
val entryValues = listEntryValues.toTypedArray<CharSequence>()
|
||||
runOnUiThread {
|
||||
val instance = findPreference<ListPreference>("selectInstance")
|
||||
instance?.entries = entries
|
||||
instance?.entryValues = entryValues
|
||||
instance?.summaryProvider =
|
||||
Preference.SummaryProvider<ListPreference> { preference ->
|
||||
val text = preference.entry
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
"kavin.rocks (Official)"
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||
this ?: return
|
||||
if (!isAdded) return // Fragment not attached to an Activity
|
||||
activity?.runOnUiThread(action)
|
||||
}
|
||||
|
||||
private fun importSubscriptions() {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
// check StorageAccess
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Log.d("myz", "" + Build.VERSION.SDK_INT)
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this.requireContext(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this.requireActivity(),
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.MANAGE_EXTERNAL_STORAGE
|
||||
),
|
||||
1
|
||||
) // permission request code is just an int
|
||||
} else if (token != "") {
|
||||
MainSettings.getContent.launch("*/*")
|
||||
} else {
|
||||
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this.requireActivity(),
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
1
|
||||
)
|
||||
} else if (token != "") {
|
||||
MainSettings.getContent.launch("*/*")
|
||||
} else {
|
||||
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribe(channels: List<String>) {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val response = try {
|
||||
val token = PreferenceHelper.getToken(requireContext())
|
||||
RetrofitInstance.authApi.importSubscriptions(
|
||||
false,
|
||||
token,
|
||||
channels
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
return@launchWhenCreated
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "HttpException, unexpected response$e")
|
||||
return@launchWhenCreated
|
||||
}
|
||||
if (response.message == "ok") {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.importsuccess,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,29 @@
|
||||
package com.github.libretube.preferences
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.github.libretube.BuildConfig
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.dialogs.RequireRestartDialog
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
import com.github.libretube.util.checkUpdate
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.dialogs.UpdateDialog
|
||||
import com.github.libretube.update.UpdateChecker
|
||||
import com.github.libretube.views.MaterialPreferenceFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainSettings : PreferenceFragmentCompat() {
|
||||
class MainSettings : MaterialPreferenceFragment() {
|
||||
val TAG = "SettingsFragment"
|
||||
|
||||
companion object {
|
||||
lateinit var getContent: ActivityResultLauncher<String>
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
|
||||
val region = findPreference<Preference>("region")
|
||||
region?.setOnPreferenceChangeListener { _, _ ->
|
||||
val restartDialog = RequireRestartDialog()
|
||||
restartDialog.show(childFragmentManager, "RequireRestartDialog")
|
||||
true
|
||||
}
|
||||
|
||||
val language = findPreference<ListPreference>("language")
|
||||
language?.setOnPreferenceChangeListener { _, _ ->
|
||||
ThemeHelper.restartMainActivity(requireContext())
|
||||
val general = findPreference<Preference>("general")
|
||||
general?.setOnPreferenceClickListener {
|
||||
val newFragment = GeneralSettings()
|
||||
navigateToSettingsFragment(newFragment)
|
||||
true
|
||||
}
|
||||
|
||||
@ -64,6 +55,20 @@ class MainSettings : PreferenceFragmentCompat() {
|
||||
true
|
||||
}
|
||||
|
||||
val history = findPreference<Preference>("history")
|
||||
history?.setOnPreferenceClickListener {
|
||||
val newFragment = HistorySettings()
|
||||
navigateToSettingsFragment(newFragment)
|
||||
true
|
||||
}
|
||||
|
||||
val notifications = findPreference<Preference>("notifications")
|
||||
notifications?.setOnPreferenceClickListener {
|
||||
val newFragment = NotificationSettings()
|
||||
navigateToSettingsFragment(newFragment)
|
||||
true
|
||||
}
|
||||
|
||||
val advanced = findPreference<Preference>("advanced")
|
||||
advanced?.setOnPreferenceClickListener {
|
||||
val newFragment = AdvancedSettings()
|
||||
@ -72,29 +77,48 @@ class MainSettings : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
val update = findPreference<Preference>("update")
|
||||
update?.title = getString(R.string.version, BuildConfig.VERSION_NAME)
|
||||
|
||||
// set the version of the update preference
|
||||
val versionString = if (BuildConfig.DEBUG) "${BuildConfig.VERSION_NAME} Debug"
|
||||
else getString(R.string.version, BuildConfig.VERSION_NAME)
|
||||
update?.title = versionString
|
||||
|
||||
// checking for update: yes -> dialog, no -> snackBar
|
||||
update?.setOnPreferenceClickListener {
|
||||
checkUpdate(childFragmentManager)
|
||||
true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// check for update
|
||||
val updateInfo = UpdateChecker.getLatestReleaseInfo()
|
||||
if (updateInfo?.name == null) {
|
||||
// request failed
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
val snackBar = Snackbar
|
||||
.make(
|
||||
settingsActivity.binding.root,
|
||||
R.string.unknown_error,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.show()
|
||||
} else if (BuildConfig.VERSION_NAME != updateInfo.name) {
|
||||
// show the UpdateAvailableDialog if there's an update available
|
||||
val updateAvailableDialog = UpdateDialog(updateInfo)
|
||||
updateAvailableDialog.show(childFragmentManager, UpdateDialog::class.java.name)
|
||||
} else {
|
||||
// otherwise show the no update available snackBar
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
val snackBar = Snackbar
|
||||
.make(
|
||||
settingsActivity.binding.root,
|
||||
R.string.app_uptodate,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
snackBar.show()
|
||||
}
|
||||
|
||||
val about = findPreference<Preference>("about")
|
||||
about?.setOnPreferenceClickListener {
|
||||
val newFragment = AboutFragment()
|
||||
navigateToSettingsFragment(newFragment)
|
||||
true
|
||||
}
|
||||
|
||||
val community = findPreference<Preference>("community")
|
||||
community?.setOnPreferenceClickListener {
|
||||
val newFragment = CommunityFragment()
|
||||
navigateToSettingsFragment(newFragment)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToSettingsFragment(newFragment: Fragment) {
|
||||
Globals.isCurrentViewMainSettings = false
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings, newFragment)
|
||||
.commitNow()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -2,12 +2,14 @@ package com.github.libretube.preferences
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.views.MaterialPreferenceFragment
|
||||
import java.util.*
|
||||
|
||||
class PlayerSettings : PreferenceFragmentCompat() {
|
||||
class PlayerSettings : MaterialPreferenceFragment() {
|
||||
val TAG = "PlayerSettings"
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@ -16,13 +18,14 @@ class PlayerSettings : PreferenceFragmentCompat() {
|
||||
val settingsActivity = activity as SettingsActivity
|
||||
settingsActivity.changeTopBarText(getString(R.string.audio_video))
|
||||
|
||||
val playerOrientation = findPreference<ListPreference>("fullscreen_orientation")
|
||||
val autoRotateToFullscreen = findPreference<SwitchPreferenceCompat>("auto_fullscreen")
|
||||
val playerOrientation =
|
||||
findPreference<ListPreference>(PreferenceKeys.FULLSCREEN_ORIENTATION)
|
||||
val autoRotateToFullscreen =
|
||||
findPreference<SwitchPreferenceCompat>(PreferenceKeys.AUTO_FULLSCREEN)
|
||||
|
||||
// only show the player orientation option if auto fullscreen is disabled
|
||||
playerOrientation?.isEnabled != PreferenceHelper.getBoolean(
|
||||
requireContext(),
|
||||
"auto_fullscreen",
|
||||
PreferenceKeys.AUTO_FULLSCREEN,
|
||||
false
|
||||
)
|
||||
|
||||
@ -30,5 +33,26 @@ class PlayerSettings : PreferenceFragmentCompat() {
|
||||
playerOrientation?.isEnabled = newValue != true
|
||||
true
|
||||
}
|
||||
|
||||
val defaultSubtitle = findPreference<ListPreference>(PreferenceKeys.DEFAULT_SUBTITLE)
|
||||
val locales: Array<Locale> = Locale.getAvailableLocales()
|
||||
val localeNames = ArrayList<String>()
|
||||
val localeCodes = ArrayList<String>()
|
||||
|
||||
localeNames.add(context?.getString(R.string.none)!!)
|
||||
localeCodes.add("")
|
||||
|
||||
locales.forEach {
|
||||
if (!localeNames.contains(it.getDisplayLanguage())) {
|
||||
localeNames.add(it.getDisplayLanguage())
|
||||
localeCodes.add(it.language)
|
||||
}
|
||||
}
|
||||
defaultSubtitle?.entries = localeNames.toTypedArray()
|
||||
defaultSubtitle?.entryValues = localeCodes.toTypedArray()
|
||||
defaultSubtitle?.summaryProvider =
|
||||
Preference.SummaryProvider<ListPreference> { preference ->
|
||||
preference.entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,176 +3,192 @@ package com.github.libretube.preferences
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.github.libretube.obj.CustomInstance
|
||||
import com.github.libretube.obj.Streams
|
||||
import com.github.libretube.obj.WatchHistoryItem
|
||||
import com.github.libretube.obj.WatchPosition
|
||||
import com.google.common.reflect.TypeToken
|
||||
import com.google.gson.Gson
|
||||
import java.lang.reflect.Type
|
||||
import com.github.libretube.util.toID
|
||||
|
||||
object PreferenceHelper {
|
||||
private val TAG = "PreferenceHelper"
|
||||
|
||||
fun setString(context: Context, key: String?, value: String?) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
editor.putString(key, value)
|
||||
editor.apply()
|
||||
private lateinit var prefContext: Context
|
||||
private lateinit var settings: SharedPreferences
|
||||
private lateinit var editor: SharedPreferences.Editor
|
||||
private val mapper = ObjectMapper()
|
||||
|
||||
/**
|
||||
* set the context that is being used to access the shared preferences
|
||||
*/
|
||||
fun setContext(context: Context) {
|
||||
prefContext = context
|
||||
settings = getDefaultSharedPreferences(prefContext)
|
||||
editor = settings.edit()
|
||||
}
|
||||
|
||||
fun setInt(context: Context, key: String?, value: Int) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
editor.putInt(key, value)
|
||||
editor.apply()
|
||||
fun getString(key: String?, defValue: String?): String {
|
||||
return settings.getString(key, defValue)!!
|
||||
}
|
||||
|
||||
fun setLong(context: Context, key: String?, value: Long) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
editor.putLong(key, value)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun setBoolean(context: Context, key: String?, value: Boolean) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
editor.putBoolean(key, value)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun getString(context: Context, key: String?, defValue: String?): String? {
|
||||
val settings: SharedPreferences = getDefaultSharedPreferences(context)
|
||||
return settings.getString(key, defValue)
|
||||
}
|
||||
|
||||
fun getInt(context: Context, key: String?, defValue: Int): Int {
|
||||
val settings: SharedPreferences = getDefaultSharedPreferences(context)
|
||||
return settings.getInt(key, defValue)
|
||||
}
|
||||
|
||||
fun getLong(context: Context, key: String?, defValue: Long): Long {
|
||||
val settings: SharedPreferences = getDefaultSharedPreferences(context)
|
||||
return settings.getLong(key, defValue)
|
||||
}
|
||||
|
||||
fun getBoolean(context: Context, key: String?, defValue: Boolean): Boolean {
|
||||
val settings: SharedPreferences = getDefaultSharedPreferences(context)
|
||||
fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
return settings.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
fun clearPreferences(context: Context) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
fun clearPreferences() {
|
||||
editor.clear().apply()
|
||||
}
|
||||
|
||||
fun removePreference(context: Context, value: String?) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
fun removePreference(value: String?) {
|
||||
editor.remove(value).apply()
|
||||
}
|
||||
|
||||
fun getToken(context: Context): String {
|
||||
val sharedPref = context.getSharedPreferences("token", Context.MODE_PRIVATE)
|
||||
fun getToken(): String {
|
||||
val sharedPref = prefContext.getSharedPreferences("token", Context.MODE_PRIVATE)
|
||||
return sharedPref?.getString("token", "")!!
|
||||
}
|
||||
|
||||
fun setToken(context: Context, newValue: String) {
|
||||
val editor = context.getSharedPreferences("token", Context.MODE_PRIVATE).edit()
|
||||
fun setToken(newValue: String) {
|
||||
val editor = prefContext.getSharedPreferences("token", Context.MODE_PRIVATE).edit()
|
||||
editor.putString("token", newValue).apply()
|
||||
}
|
||||
|
||||
fun getUsername(context: Context): String {
|
||||
val sharedPref = context.getSharedPreferences("username", Context.MODE_PRIVATE)
|
||||
fun getUsername(): String {
|
||||
val sharedPref = prefContext.getSharedPreferences("username", Context.MODE_PRIVATE)
|
||||
return sharedPref.getString("username", "")!!
|
||||
}
|
||||
|
||||
fun setUsername(context: Context, newValue: String) {
|
||||
val editor = context.getSharedPreferences("username", Context.MODE_PRIVATE).edit()
|
||||
fun setUsername(newValue: String) {
|
||||
val editor = prefContext.getSharedPreferences("username", Context.MODE_PRIVATE).edit()
|
||||
editor.putString("username", newValue).apply()
|
||||
}
|
||||
|
||||
fun saveCustomInstance(context: Context, customInstance: CustomInstance) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
val gson = Gson()
|
||||
|
||||
val customInstancesList = getCustomInstances(context)
|
||||
fun saveCustomInstance(customInstance: CustomInstance) {
|
||||
val customInstancesList = getCustomInstances()
|
||||
customInstancesList += customInstance
|
||||
|
||||
val json = gson.toJson(customInstancesList)
|
||||
val json = mapper.writeValueAsString(customInstancesList)
|
||||
editor.putString("customInstances", json).apply()
|
||||
}
|
||||
|
||||
fun getCustomInstances(context: Context): ArrayList<CustomInstance> {
|
||||
val settings = getDefaultSharedPreferences(context)
|
||||
val gson = Gson()
|
||||
fun getCustomInstances(): ArrayList<CustomInstance> {
|
||||
val json: String = settings.getString("customInstances", "")!!
|
||||
val type: Type = object : TypeToken<List<CustomInstance?>?>() {}.type
|
||||
val type = mapper.typeFactory.constructCollectionType(
|
||||
List::class.java,
|
||||
CustomInstance::class.java
|
||||
)
|
||||
return try {
|
||||
gson.fromJson(json, type)
|
||||
mapper.readValue(json, type)
|
||||
} catch (e: Exception) {
|
||||
arrayListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun getHistory(context: Context): List<String> {
|
||||
fun getSearchHistory(): List<String> {
|
||||
return try {
|
||||
val settings = getDefaultSharedPreferences(context)
|
||||
val set: Set<String> = settings.getStringSet("search_history", HashSet())!!
|
||||
set.toList()
|
||||
val json = settings.getString("search_history", "")!!
|
||||
val type = object : TypeReference<List<String>>() {}
|
||||
return mapper.readValue(json, type)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveHistory(context: Context, historyList: List<String>) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
val set: Set<String> = HashSet(historyList)
|
||||
editor.putStringSet("search_history", set).apply()
|
||||
fun saveToSearchHistory(query: String) {
|
||||
val historyList = getSearchHistory().toMutableList()
|
||||
|
||||
if ((historyList.contains(query))) {
|
||||
// remove from history list if already contained
|
||||
historyList -= query
|
||||
}
|
||||
|
||||
fun addToWatchHistory(context: Context, videoId: String, streams: Streams) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
val gson = Gson()
|
||||
// append new query to history
|
||||
historyList.add(0, query)
|
||||
|
||||
if (historyList.size > 10) {
|
||||
historyList.removeAt(historyList.size - 1)
|
||||
}
|
||||
|
||||
updateSearchHistory(historyList)
|
||||
}
|
||||
|
||||
fun removeFromSearchHistory(query: String) {
|
||||
val historyList = getSearchHistory().toMutableList()
|
||||
historyList -= query
|
||||
updateSearchHistory(historyList)
|
||||
}
|
||||
|
||||
private fun updateSearchHistory(historyList: List<String>) {
|
||||
val json = mapper.writeValueAsString(historyList)
|
||||
editor.putString("search_history", json).apply()
|
||||
}
|
||||
|
||||
fun addToWatchHistory(videoId: String, streams: Streams) {
|
||||
removeFromWatchHistory(videoId)
|
||||
|
||||
val watchHistoryItem = WatchHistoryItem(
|
||||
videoId,
|
||||
streams.title,
|
||||
streams.uploadDate,
|
||||
streams.uploader,
|
||||
streams.uploaderUrl?.replace("/channel/", ""),
|
||||
streams.uploaderUrl.toID(),
|
||||
streams.uploaderAvatar,
|
||||
streams.thumbnailUrl,
|
||||
streams.duration
|
||||
)
|
||||
|
||||
val watchHistory = getWatchHistory(context)
|
||||
val watchHistory = getWatchHistory()
|
||||
|
||||
watchHistory += watchHistoryItem
|
||||
|
||||
// remove oldest item when the watch history is longer than the pref
|
||||
val maxWatchHistorySize = getString(PreferenceKeys.WATCH_HISTORY_SIZE, "unlimited")
|
||||
if (maxWatchHistorySize != "unlimited" && watchHistory.size > maxWatchHistorySize.toInt()) {
|
||||
watchHistory.removeAt(0)
|
||||
}
|
||||
|
||||
val json = mapper.writeValueAsString(watchHistory)
|
||||
editor.putString("watch_history", json).apply()
|
||||
}
|
||||
|
||||
fun removeFromWatchHistory(videoId: String) {
|
||||
val watchHistory = getWatchHistory()
|
||||
|
||||
// delete entries that have the same videoId
|
||||
var indexToRemove: Int? = null
|
||||
watchHistory.forEachIndexed { index, item ->
|
||||
if (item.videoId == videoId) indexToRemove = index
|
||||
}
|
||||
if (indexToRemove != null) watchHistory.removeAt(indexToRemove!!)
|
||||
|
||||
watchHistory += watchHistoryItem
|
||||
|
||||
val json = gson.toJson(watchHistory)
|
||||
editor.putString("watch_history", json).apply()
|
||||
if (indexToRemove == null) return
|
||||
watchHistory.removeAt(indexToRemove!!)
|
||||
val json = mapper.writeValueAsString(watchHistory)
|
||||
editor.putString("watch_history", json).commit()
|
||||
}
|
||||
|
||||
fun getWatchHistory(context: Context): ArrayList<WatchHistoryItem> {
|
||||
val settings = getDefaultSharedPreferences(context)
|
||||
val gson = Gson()
|
||||
fun removeFromWatchHistory(position: Int) {
|
||||
val watchHistory = getWatchHistory()
|
||||
watchHistory.removeAt(position)
|
||||
|
||||
val json = mapper.writeValueAsString(watchHistory)
|
||||
editor.putString("watch_history", json).commit()
|
||||
}
|
||||
|
||||
fun getWatchHistory(): ArrayList<WatchHistoryItem> {
|
||||
val json: String = settings.getString("watch_history", "")!!
|
||||
val type: Type = object : TypeToken<List<WatchHistoryItem?>?>() {}.type
|
||||
val type = mapper.typeFactory.constructCollectionType(
|
||||
List::class.java,
|
||||
WatchHistoryItem::class.java
|
||||
)
|
||||
|
||||
return try {
|
||||
gson.fromJson(json, type)
|
||||
mapper.readValue(json, type)
|
||||
} catch (e: Exception) {
|
||||
arrayListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveWatchPosition(context: Context, videoId: String, position: Long) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
|
||||
val watchPositions = getWatchPositions(context)
|
||||
fun saveWatchPosition(videoId: String, position: Long) {
|
||||
val watchPositions = getWatchPositions()
|
||||
val watchPositionItem = WatchPosition(videoId, position)
|
||||
|
||||
var indexToRemove: Int? = null
|
||||
@ -184,15 +200,12 @@ object PreferenceHelper {
|
||||
|
||||
watchPositions += watchPositionItem
|
||||
|
||||
val gson = Gson()
|
||||
val json = gson.toJson(watchPositions)
|
||||
val json = mapper.writeValueAsString(watchPositions)
|
||||
editor.putString("watch_positions", json).commit()
|
||||
}
|
||||
|
||||
fun removeWatchPosition(context: Context, videoId: String) {
|
||||
val editor = getDefaultSharedPreferencesEditor(context)
|
||||
|
||||
val watchPositions = getWatchPositions(context)
|
||||
fun removeWatchPosition(videoId: String) {
|
||||
val watchPositions = getWatchPositions()
|
||||
|
||||
var indexToRemove: Int? = null
|
||||
watchPositions.forEachIndexed { index, item ->
|
||||
@ -201,28 +214,56 @@ object PreferenceHelper {
|
||||
|
||||
if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!)
|
||||
|
||||
val gson = Gson()
|
||||
val json = gson.toJson(watchPositions)
|
||||
val json = mapper.writeValueAsString(watchPositions)
|
||||
editor.putString("watch_positions", json).commit()
|
||||
}
|
||||
|
||||
fun getWatchPositions(context: Context): ArrayList<WatchPosition> {
|
||||
val settings = getDefaultSharedPreferences(context)
|
||||
val gson = Gson()
|
||||
fun getWatchPositions(): ArrayList<WatchPosition> {
|
||||
val json: String = settings.getString("watch_positions", "")!!
|
||||
val type: Type = object : TypeToken<List<WatchPosition?>?>() {}.type
|
||||
val type = mapper.typeFactory.constructCollectionType(
|
||||
List::class.java,
|
||||
WatchPosition::class.java
|
||||
)
|
||||
|
||||
return try {
|
||||
gson.fromJson(json, type)
|
||||
mapper.readValue(json, type)
|
||||
} catch (e: Exception) {
|
||||
arrayListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun setLatestVideoId(videoId: String) {
|
||||
editor.putString(PreferenceKeys.LAST_STREAM_VIDEO_ID, videoId)
|
||||
}
|
||||
|
||||
fun getLatestVideoId(): String {
|
||||
return getString(PreferenceKeys.LAST_STREAM_VIDEO_ID, "")
|
||||
}
|
||||
|
||||
fun saveErrorLog(log: String) {
|
||||
editor.putString(PreferenceKeys.ERROR_LOG, log).commit()
|
||||
}
|
||||
|
||||
fun getErrorLog(): String {
|
||||
return getString(PreferenceKeys.ERROR_LOG, "")
|
||||
}
|
||||
|
||||
fun getLocalSubscriptions(): List<String> {
|
||||
val json = settings.getString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, "")
|
||||
return try {
|
||||
val type = object : TypeReference<List<String>>() {}
|
||||
mapper.readValue(json, type)
|
||||
} catch (e: Exception) {
|
||||
listOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocalSubscriptions(channels: List<String>) {
|
||||
val json = mapper.writeValueAsString(channels)
|
||||
editor.putString(PreferenceKeys.LOCAL_SUBSCRIPTIONS, json).commit()
|
||||
}
|
||||
|
||||
private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
private fun getDefaultSharedPreferencesEditor(context: Context): SharedPreferences.Editor {
|
||||
return getDefaultSharedPreferences(context).edit()
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package com.github.libretube.preferences
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.activities.SettingsActivity
|
||||
import com.github.libretube.views.MaterialPreferenceFragment
|
||||
|
||||
class SponsorBlockSettings : PreferenceFragmentCompat() {
|
||||
class SponsorBlockSettings : MaterialPreferenceFragment() {
|
||||
private val TAG = "SponsorBlockSettings"
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -5,8 +5,8 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.annotation.Nullable
|
||||
import com.github.libretube.PLAYER_NOTIFICATION_ID
|
||||
|
||||
class ClosingService : Service() {
|
||||
private val TAG = "ClosingService"
|
||||
@ -20,10 +20,9 @@ class ClosingService : Service() {
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
|
||||
// destroy all notifications (especially the player notification)
|
||||
// destroy the player notification when the app gets destroyed
|
||||
val nManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nManager.cancelAll()
|
||||
Log.e(TAG, "closed")
|
||||
nManager.cancel(PLAYER_NOTIFICATION_ID)
|
||||
|
||||
// Destroy the service
|
||||
stopSelf()
|
||||
|
@ -17,25 +17,26 @@ import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.github.libretube.DOWNLOAD_CHANNEL_ID
|
||||
import com.github.libretube.DOWNLOAD_FAILURE_NOTIFICATION_ID
|
||||
import com.github.libretube.DOWNLOAD_PENDING_NOTIFICATION_ID
|
||||
import com.github.libretube.DOWNLOAD_SUCCESS_NOTIFICATION_ID
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.obj.DownloadType
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.preferences.PreferenceKeys
|
||||
import java.io.File
|
||||
|
||||
var IS_DOWNLOAD_RUNNING = false
|
||||
|
||||
class DownloadService : Service() {
|
||||
val TAG = "DownloadService"
|
||||
|
||||
private lateinit var notification: NotificationCompat.Builder
|
||||
|
||||
private var downloadId: Long = -1
|
||||
private lateinit var videoId: String
|
||||
private lateinit var videoName: String
|
||||
private lateinit var videoUrl: String
|
||||
private lateinit var audioUrl: String
|
||||
private lateinit var extension: String
|
||||
private var duration: Int = 0
|
||||
private var downloadType: Int = 3
|
||||
|
||||
private lateinit var audioDir: File
|
||||
@ -44,17 +45,15 @@ class DownloadService : Service() {
|
||||
private lateinit var tempDir: File
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
IS_DOWNLOAD_RUNNING = true
|
||||
Globals.IS_DOWNLOAD_RUNNING = true
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
videoId = intent?.getStringExtra("videoId")!!
|
||||
videoName = intent?.getStringExtra("videoName")!!
|
||||
videoUrl = intent.getStringExtra("videoUrl")!!
|
||||
audioUrl = intent.getStringExtra("audioUrl")!!
|
||||
duration = intent.getIntExtra("duration", 1)
|
||||
extension = PreferenceHelper.getString(this, "video_format", ".mp4")!!
|
||||
downloadType = if (audioUrl != "" && videoUrl != "") DownloadType.MUX
|
||||
else if (audioUrl != "") DownloadType.AUDIO
|
||||
|
||||
downloadType = if (audioUrl != "") DownloadType.AUDIO
|
||||
else if (videoUrl != "") DownloadType.VIDEO
|
||||
else DownloadType.NONE
|
||||
if (downloadType != DownloadType.NONE) {
|
||||
@ -86,8 +85,8 @@ class DownloadService : Service() {
|
||||
Log.e(TAG, "Directory already have")
|
||||
}
|
||||
|
||||
val downloadLocationPref = PreferenceHelper.getString(this, "download_location", "")
|
||||
val folderName = PreferenceHelper.getString(this, "download_folder", "LibreTube")
|
||||
val downloadLocationPref = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_LOCATION, "")
|
||||
val folderName = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_FOLDER, "LibreTube")
|
||||
|
||||
val location = when (downloadLocationPref) {
|
||||
"downloads" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
|
||||
@ -111,18 +110,8 @@ class DownloadService : Service() {
|
||||
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
)
|
||||
when (downloadType) {
|
||||
DownloadType.MUX -> {
|
||||
audioDir = File(tempDir, "$videoId-audio")
|
||||
videoDir = File(tempDir, "$videoId-video")
|
||||
downloadId = downloadManagerRequest(
|
||||
getString(R.string.video),
|
||||
getString(R.string.downloading),
|
||||
videoUrl,
|
||||
videoDir
|
||||
)
|
||||
}
|
||||
DownloadType.VIDEO -> {
|
||||
videoDir = File(libretubeDir, "$videoId-video")
|
||||
videoDir = File(libretubeDir, videoName)
|
||||
downloadId = downloadManagerRequest(
|
||||
getString(R.string.video),
|
||||
getString(R.string.downloading),
|
||||
@ -131,7 +120,7 @@ class DownloadService : Service() {
|
||||
)
|
||||
}
|
||||
DownloadType.AUDIO -> {
|
||||
audioDir = File(libretubeDir, "$videoId-audio")
|
||||
audioDir = File(libretubeDir, videoName)
|
||||
downloadId = downloadManagerRequest(
|
||||
getString(R.string.audio),
|
||||
getString(R.string.downloading),
|
||||
@ -142,6 +131,7 @@ class DownloadService : Service() {
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "download error $e")
|
||||
downloadFailedNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,8 +152,6 @@ class DownloadService : Service() {
|
||||
downloadSucceededNotification()
|
||||
onDestroy()
|
||||
}
|
||||
} else {
|
||||
muxDownloadedMedia()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,7 +184,7 @@ class DownloadService : Service() {
|
||||
}
|
||||
// Creating a notification and setting its various attributes
|
||||
notification =
|
||||
NotificationCompat.Builder(this@DownloadService, "download_service")
|
||||
NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setContentTitle("LibreTube")
|
||||
.setContentText(getString(R.string.downloading))
|
||||
@ -206,70 +194,31 @@ class DownloadService : Service() {
|
||||
.setProgress(100, 0, true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
startForeground(2, notification.build())
|
||||
startForeground(DOWNLOAD_PENDING_NOTIFICATION_ID, notification.build())
|
||||
}
|
||||
|
||||
private fun downloadFailedNotification() {
|
||||
val builder = NotificationCompat.Builder(this@DownloadService, "download_service")
|
||||
val builder = NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setContentTitle(resources.getString(R.string.downloadfailed))
|
||||
.setContentText(getString(R.string.fail))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
with(NotificationManagerCompat.from(this@DownloadService)) {
|
||||
// notificationId is a unique int for each notification that you must define
|
||||
notify(3, builder.build())
|
||||
notify(DOWNLOAD_FAILURE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadSucceededNotification() {
|
||||
Log.i(TAG, "Download succeeded")
|
||||
val builder = NotificationCompat.Builder(this@DownloadService, "download_service")
|
||||
val builder = NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setContentTitle(resources.getString(R.string.success))
|
||||
.setContentText(getString(R.string.fail))
|
||||
.setContentText(getString(R.string.downloadsucceeded))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
with(NotificationManagerCompat.from(this@DownloadService)) {
|
||||
// notificationId is a unique int for each notification that you must define
|
||||
notify(4, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun muxDownloadedMedia() {
|
||||
val command = "-y -i $videoDir -i $audioDir -c copy $libretubeDir/${videoId}$extension"
|
||||
notification.setContentTitle("Muxing")
|
||||
FFmpegKit.executeAsync(
|
||||
command,
|
||||
{ session ->
|
||||
val state = session.state
|
||||
val returnCode = session.returnCode
|
||||
// CALLED WHEN SESSION IS EXECUTED
|
||||
Log.d(
|
||||
TAG,
|
||||
String.format(
|
||||
"FFmpeg process exited with state %s and rc %s.%s",
|
||||
state,
|
||||
returnCode,
|
||||
session.failStackTrace
|
||||
)
|
||||
)
|
||||
tempDir.deleteRecursively()
|
||||
if (returnCode.toString() != "0") downloadFailedNotification()
|
||||
else downloadSucceededNotification()
|
||||
onDestroy()
|
||||
},
|
||||
{
|
||||
// CALLED WHEN SESSION PRINTS LOGS
|
||||
Log.e(TAG, it.message.toString())
|
||||
}
|
||||
) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
Log.e(TAG + "stat", it.time.toString())
|
||||
/*val progress = it.time/(10*duration!!)
|
||||
if (progress<1){
|
||||
notification
|
||||
.setProgress(progressMax, progress.toInt(), false)
|
||||
service.notify(1,notification.build())
|
||||
}*/
|
||||
notify(DOWNLOAD_SUCCESS_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,7 +228,7 @@ class DownloadService : Service() {
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
IS_DOWNLOAD_RUNNING = false
|
||||
Globals.IS_DOWNLOAD_RUNNING = false
|
||||
Log.d(TAG, "dl finished!")
|
||||
stopForeground(true)
|
||||
stopService(Intent(this@DownloadService, DownloadService::class.java))
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
20
app/src/main/java/com/github/libretube/update/Asset.kt
Normal file
20
app/src/main/java/com/github/libretube/update/Asset.kt
Normal 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
|
||||
)
|
25
app/src/main/java/com/github/libretube/update/Author.kt
Normal file
25
app/src/main/java/com/github/libretube/update/Author.kt
Normal 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
|
||||
)
|
15
app/src/main/java/com/github/libretube/update/Reactions.kt
Normal file
15
app/src/main/java/com/github/libretube/update/Reactions.kt
Normal 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
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
27
app/src/main/java/com/github/libretube/update/UpdateInfo.kt
Normal file
27
app/src/main/java/com/github/libretube/update/UpdateInfo.kt
Normal 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
|
||||
)
|
25
app/src/main/java/com/github/libretube/update/Uploader.kt
Normal file
25
app/src/main/java/com/github/libretube/update/Uploader.kt
Normal 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
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user