Merge pull request #1055 from Bnyro/master

Migrate all stored data to Room database
This commit is contained in:
Bnyro 2022-08-13 23:19:25 +02:00 committed by GitHub
commit ed51cf91a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 478 additions and 332 deletions

View File

@ -4,6 +4,7 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
}
android {
@ -74,6 +75,8 @@ android {
dependencies {
//debugImplementation libs.square.leakcanary
kapt libs.room.compiler
implementation libs.androidx.appcompat
implementation libs.androidx.constraintlayout
implementation libs.androidx.legacySupport
@ -106,6 +109,8 @@ dependencies {
implementation libs.lifecycle.viewmodel
implementation libs.lifecycle.runtime
implementation libs.lifecycle.livedata
implementation libs.room
}
static def getUnixTime() {

View File

@ -49,3 +49,8 @@ const val DOWNLOAD_SUCCESS_NOTIFICATION_ID = 5
const val DOWNLOAD_CHANNEL_ID = "download_service"
const val BACKGROUND_CHANNEL_ID = "background_mode"
const val PUSH_CHANNEL_ID = "notification_worker"
/**
* Database
*/
const val DATABASE_NAME = "LibreTubeDatabase"

View File

@ -8,6 +8,7 @@ import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import androidx.work.ExistingPeriodicWorkPolicy
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.ExceptionHandler
@ -19,15 +20,20 @@ class MyApp : Application() {
super.onCreate()
/**
* initialize the needed [NotificationChannel]s for DownloadService and BackgroundMode
* Initialize the needed [NotificationChannel]s for DownloadService and BackgroundMode
*/
initializeNotificationChannels()
/**
* set the applicationContext as context for the [PreferenceHelper]
* Set the applicationContext as context for the [PreferenceHelper]
*/
PreferenceHelper.setContext(applicationContext)
/**
* Initialize the [DatabaseHolder]
*/
DatabaseHolder.initializeDatabase(this)
/**
* bypassing fileUriExposedException, see https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed
*/
@ -35,12 +41,12 @@ class MyApp : Application() {
StrictMode.setVmPolicy(builder.build())
/**
* set the api and the auth api url
* Set the api and the auth api url
*/
setRetrofitApiUrls()
/**
* initialize the notification listener in the background
* Initialize the notification listener in the background
*/
NotificationHelper.enqueueWork(this, ExistingPeriodicWorkPolicy.KEEP)
@ -52,13 +58,13 @@ class MyApp : Application() {
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
/**
* legacy preference file migration
* Legacy preference file migration
*/
prefFileMigration()
}
/**
* set the api urls needed for the [RetrofitInstance]
* Set the api urls needed for the [RetrofitInstance]
*/
private fun setRetrofitApiUrls() {
RetrofitInstance.url =

View File

@ -89,77 +89,79 @@ class MainActivity : BaseActivity() {
if (!ConnectionHelper.isNetworkAvailable(this)) {
val noInternetIntent = Intent(this, NoInternetActivity::class.java)
startActivity(noInternetIntent)
} else {
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)
// gets the surface color of the bottom navigation view
val color = SurfaceColors.getColorForElevation(this, 10F)
// sets the navigation bar color to the previously calculated color
window.navigationBarColor = color
// hide the trending page if enabled
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(PreferenceKeys.DEFAULT_TAB, "home")) {
"home" -> R.id.homeFragment
"subscriptions" -> R.id.subscriptionsFragment
"library" -> R.id.libraryFragment
else -> R.id.homeFragment
}
// set default tab as start fragment
navController.graph.setStartDestination(startFragmentId)
// navigate to the default fragment
navController.navigate(startFragmentId)
val labelVisibilityMode = when (
PreferenceHelper.getString(PreferenceKeys.LABEL_VISIBILITY, "always")
) {
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
"never" -> NavigationBarView.LABEL_VISIBILITY_UNLABELED
else -> NavigationBarView.LABEL_VISIBILITY_AUTO
}
binding.bottomNav.labelVisibilityMode = labelVisibilityMode
binding.bottomNav.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)
}
R.id.subscriptionsFragment -> {
navController.navigate(R.id.subscriptionsFragment)
}
R.id.libraryFragment -> {
navController.navigate(R.id.libraryFragment)
}
}
false
}
binding.toolbar.title = ThemeHelper.getStyledAppName(this)
finish()
return
}
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)
// gets the surface color of the bottom navigation view
val color = SurfaceColors.getColorForElevation(this, 10F)
// sets the navigation bar color to the previously calculated color
window.navigationBarColor = color
// hide the trending page if enabled
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(PreferenceKeys.DEFAULT_TAB, "home")) {
"home" -> R.id.homeFragment
"subscriptions" -> R.id.subscriptionsFragment
"library" -> R.id.libraryFragment
else -> R.id.homeFragment
}
// set default tab as start fragment
navController.graph.setStartDestination(startFragmentId)
// navigate to the default fragment
navController.navigate(startFragmentId)
val labelVisibilityMode = when (
PreferenceHelper.getString(PreferenceKeys.LABEL_VISIBILITY, "always")
) {
"always" -> NavigationBarView.LABEL_VISIBILITY_LABELED
"selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED
"never" -> NavigationBarView.LABEL_VISIBILITY_UNLABELED
else -> NavigationBarView.LABEL_VISIBILITY_AUTO
}
binding.bottomNav.labelVisibilityMode = labelVisibilityMode
binding.bottomNav.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)
}
R.id.subscriptionsFragment -> {
navController.navigate(R.id.subscriptionsFragment)
}
R.id.libraryFragment -> {
navController.navigate(R.id.libraryFragment)
}
}
false
}
binding.toolbar.title = ThemeHelper.getStyledAppName(this)
/**
* handle error logs
*/

View File

@ -4,8 +4,9 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.databinding.SearchhistoryRowBinding
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.obj.SearchHistoryItem
class SearchHistoryAdapter(
private var historyList: List<String>,
@ -29,8 +30,12 @@ class SearchHistoryAdapter(
historyText.text = historyQuery
deleteHistory.setOnClickListener {
historyList = historyList - historyQuery
PreferenceHelper.removeFromSearchHistory(historyQuery)
historyList -= historyQuery
Thread {
DatabaseHolder.database.searchHistoryDao().delete(
SearchHistoryItem(query = historyQuery)
)
}.start()
notifyDataSetChanged()
}

View File

@ -4,11 +4,11 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.database.DatabaseHelper
import com.github.libretube.databinding.WatchHistoryRowBinding
import com.github.libretube.dialogs.VideoOptionsDialog
import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.obj.WatchHistoryItem
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.setWatchProgressLength
@ -21,7 +21,7 @@ class WatchHistoryAdapter(
private val TAG = "WatchHistoryAdapter"
fun removeFromWatchHistory(position: Int) {
PreferenceHelper.removeFromWatchHistory(position)
DatabaseHelper.removeFromWatchHistory(position)
watchHistory.removeAt(position)
notifyDataSetChanged()
}
@ -54,12 +54,12 @@ class WatchHistoryAdapter(
NavigationHelper.navigateVideo(root.context, video.videoId)
}
root.setOnLongClickListener {
VideoOptionsDialog(video.videoId!!)
VideoOptionsDialog(video.videoId)
.show(childFragmentManager, VideoOptionsDialog::class.java.name)
true
}
watchProgress.setWatchProgressLength(video.videoId!!, video.duration)
watchProgress.setWatchProgressLength(video.videoId, video.duration)
}
}

View File

@ -0,0 +1,39 @@
package com.github.libretube.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.libretube.obj.CustomInstance
import com.github.libretube.obj.SearchHistoryItem
import com.github.libretube.obj.WatchHistoryItem
import com.github.libretube.obj.WatchPosition
@Database(
entities = [
WatchHistoryItem::class,
WatchPosition::class,
SearchHistoryItem::class,
CustomInstance::class
],
version = 6
)
abstract class AppDatabase : RoomDatabase() {
/**
* Watch History
*/
abstract fun watchHistoryDao(): WatchHistoryDao
/**
* Watch Positions
*/
abstract fun watchPositionDao(): WatchPositionDao
/**
* Search History
*/
abstract fun searchHistoryDao(): SearchHistoryDao
/**
* Custom Instances
*/
abstract fun customInstanceDao(): CustomInstanceDao
}

View File

@ -0,0 +1,23 @@
package com.github.libretube.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.libretube.obj.CustomInstance
@Dao
interface CustomInstanceDao {
@Query("SELECT * FROM customInstance")
fun getAll(): List<CustomInstance>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg customInstances: CustomInstance)
@Delete
fun delete(customInstance: CustomInstance)
@Query("DELETE FROM customInstance")
fun deleteAll()
}

View File

@ -0,0 +1,61 @@
package com.github.libretube.database
import com.github.libretube.obj.Streams
import com.github.libretube.obj.WatchHistoryItem
import com.github.libretube.obj.WatchPosition
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.toID
object DatabaseHelper {
fun addToWatchHistory(videoId: String, streams: Streams) {
val watchHistoryItem = WatchHistoryItem(
videoId,
streams.title,
streams.uploadDate,
streams.uploader,
streams.uploaderUrl.toID(),
streams.uploaderAvatar,
streams.thumbnailUrl,
streams.duration
)
Thread {
DatabaseHolder.database.watchHistoryDao().insertAll(watchHistoryItem)
val maxHistorySize = PreferenceHelper.getString(PreferenceKeys.WATCH_HISTORY_SIZE, "unlimited")
if (maxHistorySize == "unlimited") return@Thread
// delete the first watch history entry if the limit is reached
val watchHistory = DatabaseHolder.database.watchHistoryDao().getAll()
if (watchHistory.size > maxHistorySize.toInt()) {
DatabaseHolder.database.watchHistoryDao()
.delete(watchHistory.first())
}
}.start()
}
fun removeFromWatchHistory(index: Int) {
Thread {
DatabaseHolder.database.watchHistoryDao().delete(
DatabaseHolder.database.watchHistoryDao().getAll()[index]
)
}.start()
}
fun saveWatchPosition(videoId: String, position: Long) {
val watchPosition = WatchPosition(
videoId,
position
)
Thread {
DatabaseHolder.database.watchPositionDao().insertAll(watchPosition)
}.start()
}
fun removeWatchPosition(videoId: String) {
Thread {
DatabaseHolder.database.watchPositionDao().delete(
DatabaseHolder.database.watchPositionDao().findById(videoId)
)
}.start()
}
}

View File

@ -0,0 +1,19 @@
package com.github.libretube.database
import android.content.Context
import androidx.room.Room
import com.github.libretube.DATABASE_NAME
object DatabaseHolder {
lateinit var database: AppDatabase
fun initializeDatabase(context: Context) {
database = Room.databaseBuilder(
context,
AppDatabase::class.java,
DATABASE_NAME
)
.fallbackToDestructiveMigration()
.build()
}
}

View File

@ -0,0 +1,23 @@
package com.github.libretube.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.libretube.obj.SearchHistoryItem
@Dao
interface SearchHistoryDao {
@Query("SELECT * FROM searchHistoryItem")
fun getAll(): List<SearchHistoryItem>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg searchHistoryItem: SearchHistoryItem)
@Delete
fun delete(searchHistoryItem: SearchHistoryItem)
@Query("DELETE FROM searchHistoryItem")
fun deleteAll()
}

View File

@ -0,0 +1,26 @@
package com.github.libretube.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.libretube.obj.WatchHistoryItem
@Dao
interface WatchHistoryDao {
@Query("SELECT * FROM watchHistoryItem")
fun getAll(): List<WatchHistoryItem>
@Query("SELECT * FROM watchHistoryItem WHERE videoId LIKE :videoId LIMIT 1")
fun findById(videoId: String): WatchHistoryItem
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg watchHistoryItems: WatchHistoryItem)
@Delete
fun delete(watchHistoryItem: WatchHistoryItem)
@Query("DELETE FROM watchHistoryItem")
fun deleteAll()
}

View File

@ -0,0 +1,26 @@
package com.github.libretube.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.libretube.obj.WatchPosition
@Dao
interface WatchPositionDao {
@Query("SELECT * FROM watchPosition")
fun getAll(): List<WatchPosition>
@Query("SELECT * FROM watchPosition WHERE videoId LIKE :videoId LIMIT 1")
fun findById(videoId: String): WatchPosition
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg watchPositions: WatchPosition)
@Delete
fun delete(watchPosition: WatchPosition)
@Query("DELETE FROM watchPosition")
fun deleteAll()
}

View File

@ -5,9 +5,9 @@ import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.databinding.DialogCustomInstanceBinding
import com.github.libretube.obj.CustomInstance
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.net.URL
@ -26,10 +26,11 @@ class CustomInstanceDialog : DialogFragment() {
}
binding.addInstance.setOnClickListener {
val customInstance = CustomInstance()
customInstance.name = binding.instanceName.text.toString()
customInstance.apiUrl = binding.instanceApiUrl.text.toString()
customInstance.frontendUrl = binding.instanceFrontendUrl.text.toString()
val customInstance = CustomInstance(
name = binding.instanceName.text.toString(),
apiUrl = binding.instanceApiUrl.text.toString(),
frontendUrl = binding.instanceFrontendUrl.text.toString()
)
if (
customInstance.name != "" &&
@ -41,7 +42,10 @@ class CustomInstanceDialog : DialogFragment() {
URL(customInstance.apiUrl).toURI()
URL(customInstance.frontendUrl).toURI()
PreferenceHelper.saveCustomInstance(customInstance)
Thread {
DatabaseHolder.database.customInstanceDao().insertAll(customInstance)
}.start()
activity?.recreate()
dismiss()
} catch (e: Exception) {

View File

@ -7,6 +7,9 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.PIPED_FRONTEND_URL
import com.github.libretube.R
import com.github.libretube.YOUTUBE_FRONTEND_URL
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.extensions.await
import com.github.libretube.obj.CustomInstance
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -71,7 +74,10 @@ class ShareDialog(
)
// get the api urls of the other custom instances
val customInstances = PreferenceHelper.getCustomInstances()
var customInstances = listOf<CustomInstance>()
Thread {
customInstances = DatabaseHolder.database.customInstanceDao().getAll()
}.await()
// return the custom instance frontend url if available
customInstances.forEach { instance ->

View File

@ -0,0 +1,8 @@
package com.github.libretube.extensions
fun Thread.await() {
this.apply {
start()
join()
}
}

View File

@ -3,37 +3,38 @@ package com.github.libretube.util
import android.view.View
import android.view.ViewTreeObserver
import android.widget.LinearLayout
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.extensions.await
/**
* 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
var progress: Long? = null
Thread {
try {
progress = DatabaseHolder.database.watchPositionDao().findById(videoId).position
} catch (e: Exception) {
progress = null
}
}.await()
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 {
if (progress == null) {
view.visibility = View.GONE
return
}
val fullWidth = (parent as LinearLayout).width
val newWidth = (fullWidth * (progress!! / (duration))) / 1000
val lp = view.layoutParams
lp.width = newWidth.toInt()
view.layoutParams = lp
view.visibility = View.VISIBLE
}
})
}

View File

@ -37,6 +37,8 @@ import com.github.libretube.activities.MainActivity
import com.github.libretube.adapters.ChaptersAdapter
import com.github.libretube.adapters.CommentsAdapter
import com.github.libretube.adapters.TrendingAdapter
import com.github.libretube.database.DatabaseHelper
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.FragmentPlayerBinding
@ -44,6 +46,7 @@ import com.github.libretube.dialogs.AddToPlaylistDialog
import com.github.libretube.dialogs.DownloadDialog
import com.github.libretube.dialogs.ShareDialog
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.extensions.await
import com.github.libretube.interfaces.DoubleTapInterface
import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.obj.ChapterSegment
@ -88,7 +91,6 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.exoplayer2.video.VideoSize
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.bottom_sheet.repeatMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -444,8 +446,7 @@ class PlayerFragment : BaseFragment() {
val newParams = if (index != 0) {
// caption selected
// get the caption name and language
val captionLanguage = subtitlesNamesList[index]
// get the caption language code
val captionLanguageCode = subtitleCodesList[index]
// select the new caption preference
@ -814,13 +815,13 @@ class PlayerFragment : BaseFragment() {
// save the watch position if video isn't finished and option enabled
private fun saveWatchPosition() {
if (watchPositionsEnabled && exoPlayer.currentPosition != exoPlayer.duration) {
PreferenceHelper.saveWatchPosition(
DatabaseHelper.saveWatchPosition(
videoId!!,
exoPlayer.currentPosition
)
} else if (watchPositionsEnabled) {
// delete watch position if video has ended
PreferenceHelper.removeWatchPosition(videoId!!)
DatabaseHelper.removeWatchPosition(videoId!!)
}
}
@ -878,7 +879,7 @@ class PlayerFragment : BaseFragment() {
if (!relatedStreamsEnabled) toggleComments()
// prepare for autoplay
if (autoplayEnabled) setNextStream()
if (watchHistoryEnabled) PreferenceHelper.addToWatchHistory(videoId!!, streams)
if (watchHistoryEnabled) DatabaseHelper.addToWatchHistory(videoId!!, streams)
}
}
}
@ -934,14 +935,14 @@ class PlayerFragment : BaseFragment() {
private fun seekToWatchPosition() {
// seek to saved watch position if available
val watchPositions = PreferenceHelper.getWatchPositions()
var position: Long? = null
watchPositions.forEach {
if (it.videoId == videoId &&
// don't seek to the position if it's the end, autoplay would skip it immediately
streams.duration!! - it.position / 1000 > 2
) position = it.position
}
Thread {
try {
position = DatabaseHolder.database.watchPositionDao().findById(videoId!!).position
} catch (e: Exception) {
position = null
}
}.await()
// support for time stamped links
val timeStamp: Long? = arguments?.getLong("timeStamp")
if (timeStamp != null && timeStamp != 0L) {

View File

@ -11,10 +11,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.activities.MainActivity
import com.github.libretube.adapters.SearchHistoryAdapter
import com.github.libretube.adapters.SearchSuggestionsAdapter
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.databinding.FragmentSearchBinding
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.extensions.await
import com.github.libretube.models.SearchViewModel
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.RetrofitInstance
import retrofit2.HttpException
import java.io.IOException
@ -89,7 +90,11 @@ class SearchFragment() : BaseFragment() {
}
private fun showHistory() {
val historyList = PreferenceHelper.getSearchHistory()
var historyList = listOf<String>()
Thread {
val history = DatabaseHolder.database.searchHistoryDao().getAll()
historyList = history.map { it.query!! }
}.await()
if (historyList.isNotEmpty()) {
binding.suggestionsRecycler.adapter =
SearchHistoryAdapter(

View File

@ -9,8 +9,11 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.adapters.SearchAdapter
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.databinding.FragmentSearchResultBinding
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.extensions.await
import com.github.libretube.obj.SearchHistoryItem
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance
@ -132,7 +135,13 @@ class SearchResultFragment : BaseFragment() {
val searchHistoryEnabled =
PreferenceHelper.getBoolean(PreferenceKeys.SEARCH_HISTORY_TOGGLE, true)
if (searchHistoryEnabled && query != "") {
PreferenceHelper.saveToSearchHistory(query)
Thread {
DatabaseHolder.database.searchHistoryDao().insertAll(
SearchHistoryItem(
query = query
)
)
}.await()
}
}
}

View File

@ -8,9 +8,11 @@ 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.database.DatabaseHolder
import com.github.libretube.databinding.FragmentWatchHistoryBinding
import com.github.libretube.extensions.BaseFragment
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.extensions.await
import com.github.libretube.obj.WatchHistoryItem
class WatchHistoryFragment : BaseFragment() {
private val TAG = "WatchHistoryFragment"
@ -28,7 +30,11 @@ class WatchHistoryFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val watchHistory = PreferenceHelper.getWatchHistory()
var watchHistory = listOf<WatchHistoryItem>()
Thread {
watchHistory = DatabaseHolder.database.watchHistoryDao().getAll()
}.await()
if (watchHistory.isEmpty()) return
@ -39,7 +45,7 @@ class WatchHistoryFragment : BaseFragment() {
}
val watchHistoryAdapter = WatchHistoryAdapter(
watchHistory,
watchHistory.toMutableList(),
childFragmentManager
)

View File

@ -1,7 +1,12 @@
package com.github.libretube.obj
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "customInstance")
class CustomInstance(
var name: String = "",
var apiUrl: String = "",
var frontendUrl: String = ""
@PrimaryKey var name: String = "",
@ColumnInfo var apiUrl: String = "",
@ColumnInfo var frontendUrl: String = ""
)

View File

@ -0,0 +1,9 @@
package com.github.libretube.obj
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "searchHistoryItem")
data class SearchHistoryItem(
@PrimaryKey val query: String = ""
)

View File

@ -1,12 +1,17 @@
package com.github.libretube.obj
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "watchHistoryItem")
data class WatchHistoryItem(
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
@PrimaryKey val videoId: String = "",
@ColumnInfo val title: String? = null,
@ColumnInfo val uploadDate: String? = null,
@ColumnInfo val uploader: String? = null,
@ColumnInfo val uploaderUrl: String? = null,
@ColumnInfo val uploaderAvatar: String? = null,
@ColumnInfo val thumbnailUrl: String? = null,
@ColumnInfo val duration: Long? = null
)

View File

@ -1,6 +1,11 @@
package com.github.libretube.obj
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "watchPosition")
data class WatchPosition(
val videoId: String = "",
val position: Long = 0L
@PrimaryKey val videoId: String = "",
@ColumnInfo val position: Long = 0L
)

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.preference.Preference
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.database.DatabaseHolder
import com.github.libretube.views.MaterialPreferenceFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -18,33 +19,41 @@ class HistorySettings : MaterialPreferenceFragment() {
// clear search history
val clearHistory = findPreference<Preference>(PreferenceKeys.CLEAR_SEARCH_HISTORY)
clearHistory?.setOnPreferenceClickListener {
showClearDialog(R.string.clear_history, "search_history")
showClearDialog(R.string.clear_history) {
DatabaseHolder.database.searchHistoryDao().deleteAll()
}
true
}
// clear watch history and positions
val clearWatchHistory = findPreference<Preference>(PreferenceKeys.CLEAR_WATCH_HISTORY)
clearWatchHistory?.setOnPreferenceClickListener {
showClearDialog(R.string.clear_history, "watch_history")
showClearDialog(R.string.clear_history) {
DatabaseHolder.database.watchHistoryDao().deleteAll()
}
true
}
// clear watch positions
val clearWatchPositions = findPreference<Preference>(PreferenceKeys.CLEAR_WATCH_POSITIONS)
clearWatchPositions?.setOnPreferenceClickListener {
showClearDialog(R.string.reset_watch_positions, "watch_positions")
showClearDialog(R.string.reset_watch_positions) {
DatabaseHolder.database.watchPositionDao().deleteAll()
}
true
}
}
private fun showClearDialog(title: Int, preferenceKey: String) {
private fun showClearDialog(title: Int, action: () -> Unit) {
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)
Thread {
action()
}.start()
}
.show()
}

View File

@ -1,6 +1,5 @@
package com.github.libretube.preferences
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
@ -13,10 +12,13 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.activities.SettingsActivity
import com.github.libretube.database.DatabaseHolder
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.extensions.await
import com.github.libretube.obj.CustomInstance
import com.github.libretube.util.ImportHelper
import com.github.libretube.util.PermissionHelper
import com.github.libretube.util.RetrofitInstance
@ -104,9 +106,10 @@ class InstanceSettings : MaterialPreferenceFragment() {
val clearCustomInstances = findPreference<Preference>(PreferenceKeys.CLEAR_CUSTOM_INSTANCES)
clearCustomInstances?.setOnPreferenceClickListener {
PreferenceHelper.removePreference("customInstances")
val intent = Intent(context, SettingsActivity::class.java)
startActivity(intent)
Thread {
DatabaseHolder.database.customInstanceDao().deleteAll()
}.await()
activity?.recreate()
true
}
@ -157,7 +160,10 @@ class InstanceSettings : MaterialPreferenceFragment() {
private fun initCustomInstances(instancePref: ListPreference) {
lifecycleScope.launchWhenCreated {
val customInstances = PreferenceHelper.getCustomInstances()
var customInstances = listOf<CustomInstance>()
Thread {
customInstances = DatabaseHolder.database.customInstanceDao().getAll()
}.await()
val instanceNames = arrayListOf<String>()
val instanceValues = arrayListOf<String>()

View File

@ -5,11 +5,6 @@ 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.github.libretube.util.toID
object PreferenceHelper {
private val TAG = "PreferenceHelper"
@ -75,174 +70,6 @@ object PreferenceHelper {
authEditor.putString(PreferenceKeys.USERNAME, newValue).apply()
}
fun saveCustomInstance(customInstance: CustomInstance) {
val customInstancesList = getCustomInstances()
customInstancesList += customInstance
val json = mapper.writeValueAsString(customInstancesList)
editor.putString("customInstances", json).apply()
}
fun getCustomInstances(): ArrayList<CustomInstance> {
val json: String = settings.getString("customInstances", "")!!
val type = mapper.typeFactory.constructCollectionType(
List::class.java,
CustomInstance::class.java
)
return try {
mapper.readValue(json, type)
} catch (e: Exception) {
arrayListOf()
}
}
fun getSearchHistory(): List<String> {
return try {
val json = settings.getString("search_history", "")!!
val type = object : TypeReference<List<String>>() {}
return mapper.readValue(json, type)
} catch (e: Exception) {
emptyList()
}
}
fun saveToSearchHistory(query: String) {
val historyList = getSearchHistory().toMutableList()
if ((historyList.contains(query))) {
// remove from history list if already contained
historyList -= query
}
// append new query to history
historyList.add(0, query)
if (historyList.size > 10) {
historyList.removeAt(historyList.size - 1)
}
updateSearchHistory(historyList)
}
fun 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.toID(),
streams.uploaderAvatar,
streams.thumbnailUrl,
streams.duration
)
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()
var indexToRemove: Int? = null
watchHistory.forEachIndexed { index, item ->
if (item.videoId == videoId) indexToRemove = index
}
if (indexToRemove == null) return
watchHistory.removeAt(indexToRemove!!)
val json = mapper.writeValueAsString(watchHistory)
editor.putString("watch_history", json).commit()
}
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 = mapper.typeFactory.constructCollectionType(
List::class.java,
WatchHistoryItem::class.java
)
return try {
mapper.readValue(json, type)
} catch (e: Exception) {
arrayListOf()
}
}
fun saveWatchPosition(videoId: String, position: Long) {
val watchPositions = getWatchPositions()
val watchPositionItem = WatchPosition(videoId, position)
var indexToRemove: Int? = null
watchPositions.forEachIndexed { index, item ->
if (item.videoId == videoId) indexToRemove = index
}
if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!)
watchPositions += watchPositionItem
val json = mapper.writeValueAsString(watchPositions)
editor.putString("watch_positions", json).commit()
}
fun removeWatchPosition(videoId: String) {
val watchPositions = getWatchPositions()
var indexToRemove: Int? = null
watchPositions.forEachIndexed { index, item ->
if (item.videoId == videoId) indexToRemove = index
}
if (indexToRemove != null) watchPositions.removeAt(indexToRemove!!)
val json = mapper.writeValueAsString(watchPositions)
editor.putString("watch_positions", json).commit()
}
fun getWatchPositions(): ArrayList<WatchPosition> {
val json: String = settings.getString("watch_positions", "")!!
val type = mapper.typeFactory.constructCollectionType(
List::class.java,
WatchPosition::class.java
)
return try {
mapper.readValue(json, type)
} catch (e: Exception) {
arrayListOf()
}
}
fun setLatestVideoId(videoId: String) {
editor.putString(PreferenceKeys.LAST_STREAM_VIDEO_ID, videoId)
}

View File

@ -2,6 +2,7 @@ package com.github.libretube.update
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.GITHUB_API_URL
import com.github.libretube.extensions.await
import java.net.URL
object UpdateChecker {
@ -15,10 +16,7 @@ object UpdateChecker {
versionInfo = getUpdateInfo()
} catch (e: Exception) {
}
}
thread.start()
// wait for the thread to finish
thread.join()
}.await()
// return the information about the latest version
return versionInfo

View File

@ -10,6 +10,7 @@ import android.support.v4.media.session.MediaSessionCompat
import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.activities.MainActivity
import com.github.libretube.extensions.await
import com.github.libretube.obj.Streams
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
@ -87,7 +88,7 @@ class NowPlayingNotification(
/**
* running on a new thread to prevent a NetworkMainThreadException
*/
val thread = Thread {
Thread {
try {
/**
* try to GET the thumbnail from the URL
@ -97,9 +98,7 @@ class NowPlayingNotification(
} catch (ex: java.lang.Exception) {
ex.printStackTrace()
}
}
thread.start()
thread.join()
}.await()
/**
* returns the scaled bitmap if it got fetched successfully
*/

View File

@ -20,6 +20,7 @@ cronetEmbedded = "101.4951.41"
cronetOkHttp = "0.1.0"
coil = "2.1.0"
leakcanary = "2.8.1"
room = "2.4.3"
[libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
@ -48,4 +49,6 @@ coil = { group = "io.coil-kt", name = "coil", version.ref="coil" }
square-leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" }
lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
room = { group = "androidx.room", name="room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }