Merge remote-tracking branch 'origin/master'

This commit is contained in:
Praveen Rajput 2023-02-06 01:08:44 +05:30
commit aa998d80a4
177 changed files with 1611 additions and 1560 deletions

View File

@ -1,18 +1,18 @@
<div align="center">
<img src="https://libre-tube.github.io/images/gh-banner.png" width="auto" height="auto" alt="LibreTube">
[![GPL-v3](https://libre-tube.github.io/images/license-widget.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Matrix](https://libre-tube.github.io/images/mat-widget.svg)](https://matrix.to/#/#LibreTube:matrix.org)
[![Telegram](https://libre-tube.github.io/images/tg-widget.svg)](https://t.me/libretube)
[![Twitter](https://libre-tube.github.io/images/tw-widget.svg)](https://twitter.com/libretube)
[![Reddit](https://libre-tube.github.io/images/rd-widget.svg)](https://www.reddit.com/r/Libretube/)
[![Discord](https://libre-tube.github.io/images/discord-widget.svg)](https://discord.gg/Qc34xCj2GV)
[![GPL-v3](https://libre-tube.github.io/assets/widgets/license-widget.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Matrix](https://libre-tube.github.io/assets/widgets/mat-widget.svg)](https://matrix.to/#/#LibreTube:matrix.org)
[![Mastodon](https://libre-tube.github.io/assets/widgets/mast-widget.svg)](https://fosstodon.org/@libretube)
[![Telegram](https://libre-tube.github.io/assets/widgets/tg-widget.svg)](https://t.me/libretube)
[![Reddit](https://libre-tube.github.io/assets/widgets/rd-widget.svg)](https://www.reddit.com/r/Libretube/)
[![Discord](https://libre-tube.github.io/assets/widgets/discord-widget.svg)](https://discord.gg/Qc34xCj2GV)
</div><div align="center" style="width:100%; display:flex; justify-content:space-between;">
[<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/assets/badges/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/badges/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/badges/ghload.png" alt="Get it on GitHub" width="30%">](https://github.com/libre-tube/LibreTube/releases/latest)
</div>

View File

@ -46,8 +46,6 @@ android {
}
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable true
applicationIdSuffix ".debug"
resValue "string", "app_name", "LibreTube Debug"
@ -127,4 +125,4 @@ dependencies {
static def getUnixTime() {
return Instant.now().getEpochSecond()
}
}

View File

@ -12,12 +12,12 @@ import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
import com.github.libretube.constants.PUSH_CHANNEL_ID
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NotificationHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.helpers.ShortcutHelper
import com.github.libretube.util.ExceptionHandler
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NotificationHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ProxyHelper
import com.github.libretube.util.ShortcutHelper
class LibreTubeApp : Application() {
override fun onCreate() {

View File

@ -109,7 +109,14 @@ interface PipedApi {
suspend fun getFeed(@Query("authToken") token: String?): List<StreamItem>
@GET("feed/unauthenticated")
suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List<StreamItem>
suspend fun getUnauthenticatedFeed(
@Query("channels") channels: String
): List<StreamItem>
@POST("feed/unauthenticated")
suspend fun getUnauthenticatedFeed(
@Body channels: List<String>
): List<StreamItem>
@GET("subscribed")
suspend fun isSubscribed(
@ -121,7 +128,14 @@ interface PipedApi {
suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription>
@GET("subscriptions/unauthenticated")
suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List<Subscription>
suspend fun unauthenticatedSubscriptions(
@Query("channels") channels: String
): List<Subscription>
@POST("subscriptions/unauthenticated")
suspend fun unauthenticatedSubscriptions(
@Body channels: List<String>
): List<Subscription>
@POST("subscribe")
suspend fun subscribe(

View File

@ -2,6 +2,7 @@ package com.github.libretube.api
import android.content.Context
import android.util.Log
import androidx.core.text.isDigitsOnly
import com.github.libretube.R
import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.PlaylistId
@ -12,20 +13,17 @@ import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toLocalPlaylistItem
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.ImportPlaylist
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ProxyHelper
import java.io.IOException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
@ -35,26 +33,22 @@ object PlaylistsHelper {
private val token get() = PreferenceHelper.getToken()
val loggedIn: Boolean get() = token != ""
val loggedIn: Boolean get() = token.isNotEmpty()
suspend fun getPlaylists(): List<Playlists> {
if (loggedIn) return RetrofitInstance.authApi.getUserPlaylists(token)
val localPlaylists = awaitQuery {
suspend fun getPlaylists(): List<Playlists> = withContext(Dispatchers.IO) {
if (loggedIn) {
RetrofitInstance.authApi.getUserPlaylists(token)
} else {
DatabaseHolder.Database.localPlaylistsDao().getAll()
.map {
Playlists(
id = it.playlist.id.toString(),
name = it.playlist.name,
thumbnail = ProxyHelper.rewriteUrl(it.playlist.thumbnailUrl),
videos = it.videos.size.toLong()
)
}
}
val playlists = mutableListOf<Playlists>()
localPlaylists.forEach {
playlists.add(
Playlists(
id = it.playlist.id.toString(),
name = it.playlist.name,
thumbnail = ProxyHelper.rewriteUrl(it.playlist.thumbnailUrl),
videos = it.videos.size.toLong()
)
)
}
return playlists
}
suspend fun getPlaylist(playlistId: String): Playlist {
@ -63,9 +57,8 @@ object PlaylistsHelper {
PlaylistType.PRIVATE -> RetrofitInstance.authApi.getPlaylist(playlistId)
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
PlaylistType.LOCAL -> {
val relation = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }
val relation = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
return Playlist(
name = relation.playlist.name,
thumbnailUrl = ProxyHelper.rewriteUrl(relation.playlist.thumbnailUrl),
@ -76,38 +69,26 @@ object PlaylistsHelper {
}
}
suspend fun createPlaylist(
playlistName: String,
appContext: Context
): String? {
suspend fun createPlaylist(playlistName: String, appContext: Context?): String? {
if (!loggedIn) {
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().createPlaylist(
LocalPlaylist(
name = playlistName,
thumbnailUrl = ""
)
)
val playlist = LocalPlaylist(name = playlistName, thumbnailUrl = "")
DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist)
return DatabaseHolder.Database.localPlaylistsDao().getAll()
.last().playlist.id.toString()
} else {
return try {
RetrofitInstance.authApi.createPlaylist(token, Playlists(name = playlistName))
} catch (e: IOException) {
appContext?.toastFromMainThread(R.string.unknown_error)
return null
} catch (e: HttpException) {
Log.e(TAG(), e.toString())
appContext?.toastFromMainThread(R.string.server_error)
return null
}.playlistId.also {
appContext?.toastFromMainThread(R.string.playlistCreated)
}
return awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.last().playlist.id.toString()
}
val response = try {
RetrofitInstance.authApi.createPlaylist(token, Playlists(name = playlistName))
} catch (e: IOException) {
appContext.toastFromMainThread(R.string.unknown_error)
return null
} catch (e: HttpException) {
Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.server_error)
return null
}
if (response.playlistId != null) {
appContext.toastFromMainThread(R.string.playlistCreated)
return response.playlistId
}
return null
}
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean {
@ -117,22 +98,19 @@ object PlaylistsHelper {
for (video in videos) {
val localPlaylistItem = video.toLocalPlaylistItem(playlistId)
awaitQuery {
// avoid duplicated videos in a playlist
DatabaseHolder.Database.localPlaylistsDao()
.deletePlaylistItemsByVideoId(playlistId, localPlaylistItem.videoId)
// avoid duplicated videos in a playlist
DatabaseHolder.Database.localPlaylistsDao()
.deletePlaylistItemsByVideoId(playlistId, localPlaylistItem.videoId)
// add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
// add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
if (localPlaylist.playlist.thumbnailUrl == "") {
// set the new playlist thumbnail URL
localPlaylistItem.thumbnailUrl?.let {
localPlaylist.playlist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(
localPlaylist.playlist
)
}
val playlist = localPlaylist.playlist
if (playlist.thumbnailUrl == "") {
// set the new playlist thumbnail URL
localPlaylistItem.thumbnailUrl?.let {
playlist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
}
}
@ -144,89 +122,72 @@ object PlaylistsHelper {
}
suspend fun renamePlaylist(playlistId: String, newName: String): Boolean {
if (!loggedIn) {
val playlist = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }.playlist
return if (!loggedIn) {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }.playlist
playlist.name = newName
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
return true
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
true
} else {
val playlist = PlaylistId(playlistId, newName = newName)
RetrofitInstance.authApi.renamePlaylist(token, playlist).playlistId != null
}
return RetrofitInstance.authApi.renamePlaylist(
token,
PlaylistId(playlistId, newName = newName)
).playlistId != null
}
suspend fun removeFromPlaylist(playlistId: String, index: Int) {
if (!loggedIn) {
val transaction = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(
transaction.videos[index]
)
}
if (transaction.videos.size > 1) {
if (index == 0) {
transaction.videos[1].thumbnailUrl?.let {
transaction.playlist.thumbnailUrl = it
}
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(
transaction.playlist
)
}
}
return
}
// remove thumbnail if playlist now empty
awaitQuery {
transaction.playlist.thumbnailUrl = ""
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist)
}
return
}
RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(),
PlaylistId(
playlistId = playlistId,
index = index
suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean {
return if (!loggedIn) {
val transaction = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(
transaction.videos[index]
)
)
// set a new playlist thumbnail if the first video got removed
if (index == 0) {
transaction.playlist.thumbnailUrl = transaction.videos.getOrNull(1)?.thumbnailUrl ?: ""
}
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist)
true
} else {
RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(),
PlaylistId(playlistId = playlistId, index = index)
).message == "ok"
}
}
suspend fun importPlaylists(appContext: Context, playlists: List<ImportPlaylist>) {
for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue
// if logged in, add the playlists by their ID via an api call
val success: Boolean = if (loggedIn) {
addToPlaylist(
playlistId,
*playlist.videos.map {
StreamItem(url = it)
}.toTypedArray()
)
} else {
// if not logged in, all video information needs to become fetched manually
try {
val streamItems = playlist.videos.map {
RetrofitInstance.api.getStreams(it).toStreamItem(it)
suspend fun importPlaylists(playlists: List<ImportPlaylist>) = withContext(Dispatchers.IO) {
playlists.map { playlist ->
val playlistId = createPlaylist(playlist.name!!, null)
async {
playlistId ?: return@async
// if logged in, add the playlists by their ID via an api call
if (loggedIn) {
addToPlaylist(
playlistId,
*playlist.videos.map {
StreamItem(url = it)
}.toTypedArray()
)
} else {
// if not logged in, all video information needs to become fetched manually
runCatching {
val streamItems = playlist.videos.map {
async {
try {
RetrofitInstance.api.getStreams(it).toStreamItem(it)
} catch (e: Exception) {
null
}
}
}
.awaitAll()
.filterNotNull()
addToPlaylist(playlistId, *streamItems.toTypedArray())
}
addToPlaylist(playlistId, *streamItems.toTypedArray())
} catch (e: Exception) {
false
}
}
appContext.toastFromMainThread(
if (success) R.string.importsuccess else R.string.server_error
)
}
}.awaitAll()
}
suspend fun exportPlaylists(): List<ImportPlaylist> = withContext(Dispatchers.IO) {
@ -241,49 +202,35 @@ object PlaylistsHelper {
}
}
fun clonePlaylist(context: Context, playlistId: String) {
suspend fun clonePlaylist(context: Context, playlistId: String): String? {
val appContext = context.applicationContext
if (!loggedIn) {
CoroutineScope(Dispatchers.IO).launch {
val playlist = try {
RetrofitInstance.api.getPlaylist(playlistId)
} catch (e: Exception) {
appContext.toastFromMainThread(R.string.server_error)
return@launch
}
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name", appContext)
newPlaylist ?: return@launch
addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
var nextPage = playlist.nextpage
while (nextPage != null) {
nextPage = try {
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage).apply {
addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
}.nextpage
} catch (e: Exception) {
return@launch
}
}
}
return
}
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.clonePlaylist(
token,
PlaylistId(playlistId)
)
val playlist = try {
RetrofitInstance.api.getPlaylist(playlistId)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launch
appContext.toastFromMainThread(R.string.server_error)
return null
}
appContext?.toastFromMainThread(
if (response.playlistId != null) R.string.playlistCloned else R.string.server_error
)
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name", appContext) ?: return null
addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
var nextPage = playlist.nextpage
while (nextPage != null) {
nextPage = try {
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage).apply {
addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
}.nextpage
} catch (e: Exception) {
break
}
}
return playlistId
}
return runCatching {
RetrofitInstance.authApi.clonePlaylist(token, PlaylistId(playlistId))
}.getOrNull()?.playlistId
}
fun getPrivatePlaylistType(): PlaylistType {
@ -291,8 +238,12 @@ object PlaylistsHelper {
}
private fun getPrivatePlaylistType(playlistId: String): PlaylistType {
if (playlistId.all { it.isDigit() }) return PlaylistType.LOCAL
if (playlistId.matches(pipedPlaylistRegex)) return PlaylistType.PRIVATE
return PlaylistType.PUBLIC
return if (playlistId.isDigitsOnly()) {
PlaylistType.LOCAL
} else if (playlistId.matches(pipedPlaylistRegex)) {
PlaylistType.PRIVATE
} else {
PlaylistType.PUBLIC
}
}
}

View File

@ -2,7 +2,7 @@ package com.github.libretube.api
import com.github.libretube.constants.PIPED_API_URL
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit

View File

@ -10,13 +10,15 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.extensions.TAG
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
object SubscriptionHelper {
private const val GET_SUBSCRIPTIONS_LIMIT = 100
suspend fun subscribe(channelId: String) {
val token = PreferenceHelper.getToken()
if (token.isNotEmpty()) {
@ -102,17 +104,20 @@ object SubscriptionHelper {
}
}
suspend fun getFormattedLocalSubscriptions(): String {
return Database.localSubscriptionDao().getAll()
.joinToString(",") { it.channelId }
}
suspend fun getSubscriptions(): List<Subscription> {
val token = PreferenceHelper.getToken()
return if (token.isNotEmpty()) {
RetrofitInstance.authApi.subscriptions(token)
} else {
RetrofitInstance.authApi.unauthenticatedSubscriptions(getFormattedLocalSubscriptions())
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
when {
subscriptions.size > GET_SUBSCRIPTIONS_LIMIT -> RetrofitInstance.authApi.unauthenticatedSubscriptions(
subscriptions
)
else -> RetrofitInstance.authApi.unauthenticatedSubscriptions(
subscriptions.joinToString(",")
)
}
}
}
@ -121,7 +126,15 @@ object SubscriptionHelper {
return if (token.isNotEmpty()) {
RetrofitInstance.authApi.getFeed(token)
} else {
RetrofitInstance.authApi.getUnauthenticatedFeed(getFormattedLocalSubscriptions())
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
when {
subscriptions.size > GET_SUBSCRIPTIONS_LIMIT -> RetrofitInstance.authApi.getUnauthenticatedFeed(
subscriptions
)
else -> RetrofitInstance.authApi.getUnauthenticatedFeed(
subscriptions.joinToString(",")
)
}
}
}
}

View File

@ -18,11 +18,11 @@ const val FAQ_URL = "https://libre-tube.github.io/#faq"
/**
* Social media links for the community fragment
*/
const val TELEGRAM_URL = "https://t.me/libretube"
const val MATRIX_URL = "https://matrix.to/#/#LibreTube:matrix.org"
const val MASTODON_URL = "https://fosstodon.org/@libretube"
const val TELEGRAM_URL = "https://t.me/libretube"
const val DISCORD_URL = "https://discord.com/invite/Qc34xCj2GV"
const val REDDIT_URL = "https://www.reddit.com/r/Libretube/"
const val TWITTER_URL = "https://twitter.com/libretube"
/**
* Share Dialog

View File

@ -7,7 +7,7 @@ import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
object DatabaseHelper {
private const val MAX_SEARCH_HISTORY_SIZE = 20

View File

@ -14,29 +14,26 @@ import com.github.libretube.db.obj.LocalPlaylistWithVideos
interface LocalPlaylistsDao {
@Transaction
@Query("SELECT * FROM LocalPlaylist")
fun getAll(): List<LocalPlaylistWithVideos>
suspend fun getAll(): List<LocalPlaylistWithVideos>
@Insert
fun createPlaylist(playlist: LocalPlaylist)
suspend fun createPlaylist(playlist: LocalPlaylist)
@Update
fun updatePlaylist(playlist: LocalPlaylist)
@Delete
fun deletePlaylist(playlist: LocalPlaylist)
suspend fun updatePlaylist(playlist: LocalPlaylist)
@Query("DELETE FROM localPlaylist WHERE id = :playlistId")
fun deletePlaylistById(playlistId: String)
suspend fun deletePlaylistById(playlistId: String)
@Insert
fun addPlaylistVideo(playlistVideo: LocalPlaylistItem)
suspend fun addPlaylistVideo(playlistVideo: LocalPlaylistItem)
@Delete
fun removePlaylistVideo(playlistVideo: LocalPlaylistItem)
suspend fun removePlaylistVideo(playlistVideo: LocalPlaylistItem)
@Query("DELETE FROM localPlaylistItem WHERE playlistId = :playlistId")
fun deletePlaylistItemsByPlaylistId(playlistId: String)
suspend fun deletePlaylistItemsByPlaylistId(playlistId: String)
@Query("DELETE FROM localPlaylistItem WHERE playlistId = :playlistId AND videoId = :videoId")
fun deletePlaylistItemsByVideoId(playlistId: String, videoId: String)
suspend fun deletePlaylistItemsByVideoId(playlistId: String, videoId: String)
}

View File

@ -3,7 +3,7 @@ package com.github.libretube.extensions
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams
import com.github.libretube.db.obj.LocalPlaylistItem
import com.github.libretube.util.ProxyHelper
import com.github.libretube.helpers.ProxyHelper
fun Streams.toStreamItem(videoId: String): StreamItem {
return StreamItem(

View File

@ -4,6 +4,8 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
fun Context.toastFromMainThread(text: String) {
Handler(Looper.getMainLooper()).post {
@ -18,3 +20,11 @@ fun Context.toastFromMainThread(text: String) {
fun Context.toastFromMainThread(stringId: Int) {
toastFromMainThread(getString(stringId))
}
suspend fun Context.toastFromMainDispatcher(text: String) = withContext(Dispatchers.Main) {
Toast.makeText(this@toastFromMainDispatcher, text, Toast.LENGTH_SHORT).show()
}
suspend fun Context.toastFromMainDispatcher(stringId: Int) {
toastFromMainDispatcher(getString(stringId))
}

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.media.AudioManager

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.app.ActivityManager
import android.content.Context

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.net.Uri
@ -11,8 +11,7 @@ import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.TAG
import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
@ -23,67 +22,63 @@ import kotlinx.serialization.json.longOrNull
/**
* Backup and restore the preferences
*/
class BackupHelper(private val context: Context) {
object BackupHelper {
/**
* Write a [BackupFile] containing the database content as well as the preferences
*/
fun createAdvancedBackup(uri: Uri?, backupFile: BackupFile) {
uri?.let {
try {
context.contentResolver.openOutputStream(it)?.use { outputStream ->
JsonHelper.json.encodeToStream(backupFile, outputStream)
}
} catch (e: Exception) {
Log.e(TAG(), "Error while writing backup: $e")
@OptIn(ExperimentalSerializationApi::class)
fun createAdvancedBackup(context: Context, uri: Uri, backupFile: BackupFile) {
try {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
JsonHelper.json.encodeToStream(backupFile, outputStream)
}
} catch (e: Exception) {
Log.e(TAG(), "Error while writing backup: $e")
}
}
/**
* Restore data from a [BackupFile]
*/
fun restoreAdvancedBackup(uri: Uri?) {
val backupFile = uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream ->
JsonHelper.json.decodeFromStream<BackupFile>(inputStream)
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun restoreAdvancedBackup(context: Context, uri: Uri) {
val backupFile = context.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<BackupFile>(it)
} ?: return
runBlocking(Dispatchers.IO) {
Database.watchHistoryDao().insertAll(
*backupFile.watchHistory.orEmpty().toTypedArray()
)
Database.searchHistoryDao().insertAll(
*backupFile.searchHistory.orEmpty().toTypedArray()
)
Database.watchPositionDao().insertAll(
*backupFile.watchPositions.orEmpty().toTypedArray()
)
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
Database.customInstanceDao().insertAll(
*backupFile.customInstances.orEmpty().toTypedArray()
)
Database.playlistBookmarkDao().insertAll(
*backupFile.playlistBookmarks.orEmpty().toTypedArray()
)
Database.watchHistoryDao().insertAll(
*backupFile.watchHistory.orEmpty().toTypedArray()
)
Database.searchHistoryDao().insertAll(
*backupFile.searchHistory.orEmpty().toTypedArray()
)
Database.watchPositionDao().insertAll(
*backupFile.watchPositions.orEmpty().toTypedArray()
)
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
Database.customInstanceDao().insertAll(
*backupFile.customInstances.orEmpty().toTypedArray()
)
Database.playlistBookmarkDao().insertAll(
*backupFile.playlistBookmarks.orEmpty().toTypedArray()
)
backupFile.localPlaylists.orEmpty().forEach {
Database.localPlaylistsDao().createPlaylist(it.playlist)
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
it.videos.forEach {
it.playlistId = playlistId
Database.localPlaylistsDao().addPlaylistVideo(it)
}
backupFile.localPlaylists.orEmpty().forEach {
Database.localPlaylistsDao().createPlaylist(it.playlist)
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
it.videos.forEach {
it.playlistId = playlistId
Database.localPlaylistsDao().addPlaylistVideo(it)
}
restorePreferences(backupFile.preferences)
}
restorePreferences(context, backupFile.preferences)
}
/**
* Restore the shared preferences from a backup file
*/
private fun restorePreferences(preferences: List<PreferenceItem>?) {
private fun restorePreferences(context: Context, preferences: List<PreferenceItem>?) {
if (preferences == null) return
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) {
// clear the previous settings

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.app.Activity
import android.view.WindowManager

View File

@ -0,0 +1,14 @@
package com.github.libretube.helpers
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.core.content.getSystemService
import com.github.libretube.R
object ClipboardHelper {
fun save(context: Context, text: String) {
val clip = ClipData.newPlainText(context.getString(R.string.copied), text)
context.getSystemService<ClipboardManager>()!!.setPrimaryClip(clip)
}
}

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Streams

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.content.Intent

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.graphics.Bitmap
@ -13,6 +13,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import com.github.libretube.api.CronetHelper
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.util.DataSaverMode
import java.io.File
import java.io.FileOutputStream
import okio.use

View File

@ -1,60 +1,51 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.app.Activity
import android.net.Uri
import android.util.Log
import android.widget.Toast
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.obj.ImportPlaylist
import com.github.libretube.obj.ImportPlaylistFile
import com.github.libretube.obj.NewPipeSubscription
import com.github.libretube.obj.NewPipeSubscriptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import okio.use
class ImportHelper(
private val activity: Activity
) {
object ImportHelper {
/**
* Import subscriptions by a file uri
*/
fun importSubscriptions(uri: Uri?) {
if (uri == null) return
suspend fun importSubscriptions(activity: Activity, uri: Uri) {
try {
val applicationContext = activity.applicationContext
val channels = getChannelsFromUri(uri)
CoroutineScope(Dispatchers.IO).launch {
SubscriptionHelper.importSubscriptions(channels)
}.invokeOnCompletion {
applicationContext.toastFromMainThread(R.string.importsuccess)
}
SubscriptionHelper.importSubscriptions(getChannelsFromUri(activity, uri))
activity.toastFromMainDispatcher(R.string.importsuccess)
} catch (e: IllegalArgumentException) {
Log.e(TAG(), e.toString())
activity.toastFromMainThread(
activity.toastFromMainDispatcher(
activity.getString(R.string.unsupported_file_format) +
" (${activity.contentResolver.getType(uri)})"
)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show()
e.localizedMessage?.let {
activity.toastFromMainDispatcher(it)
}
}
}
/**
* Get a list of channel IDs from a file [Uri]
*/
private fun getChannelsFromUri(uri: Uri): List<String> {
private fun getChannelsFromUri(activity: Activity, uri: Uri): List<String> {
return when (val fileType = activity.contentResolver.getType(uri)) {
"application/json", "application/*", "application/octet-stream" -> {
// NewPipe subscriptions format
@ -82,36 +73,31 @@ class ImportHelper(
/**
* Write the text to the document
*/
fun exportSubscriptions(uri: Uri?) {
if (uri == null) return
runBlocking(Dispatchers.IO) {
val token = PreferenceHelper.getToken()
val subs = if (token.isNotEmpty()) {
RetrofitInstance.authApi.subscriptions(token)
} else {
RetrofitInstance.authApi.unauthenticatedSubscriptions(
SubscriptionHelper.getFormattedLocalSubscriptions()
)
}
val newPipeChannels = subs.map {
NewPipeSubscription(it.name, 0, "https://www.youtube.com${it.url}")
}
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(newPipeSubscriptions, it)
}
activity.toastFromMainThread(R.string.exportsuccess)
suspend fun exportSubscriptions(activity: Activity, uri: Uri) {
val token = PreferenceHelper.getToken()
val subs = if (token.isNotEmpty()) {
RetrofitInstance.authApi.subscriptions(token)
} else {
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
RetrofitInstance.authApi.unauthenticatedSubscriptions(subscriptions)
}
val newPipeChannels = subs.map {
NewPipeSubscription(it.name, 0, "https://www.youtube.com${it.url}")
}
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(newPipeSubscriptions, it)
}
activity.toastFromMainDispatcher(R.string.exportsuccess)
}
/**
* Import Playlists
*/
fun importPlaylists(uri: Uri?) {
if (uri == null) return
@OptIn(ExperimentalSerializationApi::class)
suspend fun importPlaylists(activity: Activity, uri: Uri) {
val importPlaylists = mutableListOf<ImportPlaylist>()
when (val fileType = activity.contentResolver.getType(uri)) {
@ -136,20 +122,22 @@ class ImportHelper(
importPlaylists.addAll(playlistFile?.playlists.orEmpty())
}
else -> {
activity.applicationContext.toastFromMainThread("Unsupported file type $fileType")
activity.toastFromMainDispatcher("Unsupported file type $fileType")
return
}
}
CoroutineScope(Dispatchers.IO).launch {
try {
PlaylistsHelper.importPlaylists(activity, importPlaylists)
activity.applicationContext.toastFromMainThread(R.string.success)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
e.localizedMessage?.let {
activity.applicationContext.toastFromMainThread(it)
}
// convert the YouTube URLs to videoIds
importPlaylists.forEach { playlist ->
playlist.videos = playlist.videos.map { it.takeLast(11) }
}
try {
PlaylistsHelper.importPlaylists(importPlaylists)
activity.toastFromMainDispatcher(R.string.success)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
e.localizedMessage?.let {
activity.toastFromMainDispatcher(it)
}
}
}
@ -157,18 +145,14 @@ class ImportHelper(
/**
* Export Playlists
*/
fun exportPlaylists(uri: Uri?) {
if (uri == null) return
suspend fun exportPlaylists(activity: Activity, uri: Uri) {
val playlists = PlaylistsHelper.exportPlaylists()
val playlistFile = ImportPlaylistFile("Piped", 1, playlists)
runBlocking {
val playlists = PlaylistsHelper.exportPlaylists()
val playlistFile = ImportPlaylistFile("Piped", 1, playlists)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
}
activity.toastFromMainThread(R.string.exportsuccess)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
}
activity.toastFromMainDispatcher(R.string.exportsuccess)
}
}

View File

@ -0,0 +1,13 @@
package com.github.libretube.helpers
import android.content.Context
import android.content.Intent
import android.net.Uri
object IntentHelper {
fun openLinkFromHref(context: Context, link: String) {
val uri = Uri.parse(link)
val launchIntent = Intent(Intent.ACTION_VIEW).setData(uri)
context.startActivity(launchIntent)
}
}

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.content.res.Configuration

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.util.Log

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.app.NotificationManager
import android.content.Context
@ -10,6 +10,8 @@ import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.os.postDelayed
import androidx.fragment.app.commitNow
import androidx.fragment.app.replace
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
@ -90,17 +92,9 @@ object NavigationHelper {
)
val activity = context as AppCompatActivity
activity.supportFragmentManager.beginTransaction()
.remove(PlayerFragment())
.commit()
activity.supportFragmentManager.beginTransaction()
.replace(
R.id.container,
PlayerFragment().apply {
arguments = bundle
}
)
.commitNow()
activity.supportFragmentManager.commitNow {
replace<PlayerFragment>(R.id.container, args = bundle)
}
}
fun navigatePlaylist(

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.net.ConnectivityManager

View File

@ -1,10 +1,10 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.github.libretube.constants.NOTIFICATION_WORK_NAME
import com.github.libretube.constants.PreferenceKeys
@ -56,8 +56,7 @@ object NotificationHelper {
.build()
// create the worker
val notificationWorker = PeriodicWorkRequest.Builder(
NotificationWorker::class.java,
val notificationWorker = PeriodicWorkRequestBuilder<NotificationWorker>(
checkingFrequency,
TimeUnit.MINUTES
)

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.app.Activity
import android.app.PendingIntent

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.content.SharedPreferences

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.content.Context
import android.content.Intent

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.app.Activity
import android.content.ComponentName

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.helpers
import android.os.Build
import android.view.WindowManager

View File

@ -29,9 +29,9 @@ import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.PlayingQueue
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem

View File

@ -25,14 +25,14 @@ import com.github.libretube.extensions.getContentLength
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toDownloadItems
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.helpers.DownloadHelper.getNotificationId
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.obj.DownloadStatus
import com.github.libretube.receivers.NotificationReceiver
import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_PAUSE
import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_RESUME
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.util.DownloadHelper
import com.github.libretube.util.DownloadHelper.getNotificationId
import com.github.libretube.util.ImageHelper
import java.io.File
import java.net.HttpURLConnection
import java.net.SocketTimeoutException

View File

@ -11,7 +11,7 @@ import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import com.github.libretube.R
import com.github.libretube.util.DownloadHelper
import com.github.libretube.helpers.DownloadHelper
import java.io.File
class UpdateService : Service() {

View File

@ -1,10 +1,6 @@
package com.github.libretube.ui.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.core.text.HtmlCompat
@ -16,7 +12,10 @@ import com.github.libretube.constants.PIPED_GITHUB_URL
import com.github.libretube.constants.WEBLATE_URL
import com.github.libretube.constants.WEBSITE_URL
import com.github.libretube.databinding.ActivityAboutBinding
import com.github.libretube.helpers.ClipboardHelper
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.ui.base.BaseActivity
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
@ -34,7 +33,7 @@ class AboutActivity : BaseActivity() {
}
binding.appIcon.setOnClickListener {
val sendIntent: Intent = Intent().apply {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, GITHUB_URL)
type = "text/plain"
@ -44,37 +43,10 @@ class AboutActivity : BaseActivity() {
startActivity(shareIntent)
}
binding.website.setOnClickListener {
openLinkFromHref(WEBSITE_URL)
}
binding.website.setOnLongClickListener {
onLongClick(WEBSITE_URL)
true
}
binding.piped.setOnClickListener {
openLinkFromHref(PIPED_GITHUB_URL)
}
binding.piped.setOnLongClickListener {
onLongClick(PIPED_GITHUB_URL)
true
}
binding.translate.setOnClickListener {
openLinkFromHref(WEBLATE_URL)
}
binding.translate.setOnLongClickListener {
onLongClick(WEBLATE_URL)
true
}
binding.github.setOnClickListener {
openLinkFromHref(GITHUB_URL)
}
binding.github.setOnLongClickListener {
onLongClick(GITHUB_URL)
true
}
setupCard(binding.website, WEBSITE_URL)
setupCard(binding.piped, PIPED_GITHUB_URL)
setupCard(binding.translate, WEBLATE_URL)
setupCard(binding.github, GITHUB_URL)
binding.license.setOnClickListener {
showLicense()
@ -89,18 +61,19 @@ class AboutActivity : BaseActivity() {
}
}
private fun openLinkFromHref(link: String) {
val uri = Uri.parse(link)
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
startActivity(intent)
private fun setupCard(card: MaterialCardView, link: String) {
card.setOnClickListener {
IntentHelper.openLinkFromHref(this, link)
}
card.setOnLongClickListener {
onLongClick(link)
true
}
}
private fun onLongClick(href: String) {
// copy the link to the clipboard
val clipboard: ClipboardManager =
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(getString(R.string.copied), href)
clipboard.setPrimaryClip(clip)
ClipboardHelper.save(this, href)
// show the snackBar with open action
Snackbar.make(
binding.root,
@ -108,7 +81,7 @@ class AboutActivity : BaseActivity() {
Snackbar.LENGTH_LONG
)
.setAction(R.string.open_copied) {
openLinkFromHref(href)
IntentHelper.openLinkFromHref(this, href)
}
.setAnimationMode(Snackbar.ANIMATION_MODE_FADE)
.show()

View File

@ -1,16 +1,16 @@
package com.github.libretube.ui.activities
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.github.libretube.constants.DISCORD_URL
import com.github.libretube.constants.FAQ_URL
import com.github.libretube.constants.MASTODON_URL
import com.github.libretube.constants.MATRIX_URL
import com.github.libretube.constants.REDDIT_URL
import com.github.libretube.constants.TELEGRAM_URL
import com.github.libretube.constants.TWITTER_URL
import com.github.libretube.databinding.ActivityHelpBinding
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.ui.base.BaseActivity
import com.google.android.material.card.MaterialCardView
class HelpActivity : BaseActivity() {
private lateinit var binding: ActivityHelpBinding
@ -25,34 +25,17 @@ class HelpActivity : BaseActivity() {
onBackPressedDispatcher.onBackPressed()
}
binding.faq.setOnClickListener {
openLinkFromHref(FAQ_URL)
}
binding.matrix.setOnClickListener {
openLinkFromHref(MATRIX_URL)
}
binding.telegram.setOnClickListener {
openLinkFromHref(TELEGRAM_URL)
}
binding.discord.setOnClickListener {
openLinkFromHref(DISCORD_URL)
}
binding.reddit.setOnClickListener {
openLinkFromHref(REDDIT_URL)
}
binding.twitter.setOnClickListener {
openLinkFromHref(TWITTER_URL)
}
setupCard(binding.faq, FAQ_URL)
setupCard(binding.matrix, MATRIX_URL)
setupCard(binding.mastodon, MASTODON_URL)
setupCard(binding.telegram, TELEGRAM_URL)
setupCard(binding.discord, DISCORD_URL)
setupCard(binding.reddit, REDDIT_URL)
}
private fun openLinkFromHref(link: String) {
val uri = Uri.parse(link)
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
startActivity(intent)
private fun setupCard(card: MaterialCardView, link: String) {
card.setOnClickListener {
IntentHelper.openLinkFromHref(this, link)
}
}
}

View File

@ -27,6 +27,12 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.NetworkHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.helpers.WindowHelper
import com.github.libretube.services.ClosingService
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.ErrorDialog
@ -36,12 +42,6 @@ import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.tools.SleepTimer
import com.github.libretube.util.NavBarHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.NetworkHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
import com.github.libretube.util.WindowHelper
import com.google.android.material.elevation.SurfaceColors
class MainActivity : BaseActivity() {

View File

@ -4,12 +4,13 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.addCallback
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import com.github.libretube.R
import com.github.libretube.databinding.ActivityNointernetBinding
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.NetworkHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.fragments.DownloadsFragment
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.NetworkHelper
import com.google.android.material.snackbar.Snackbar
class NoInternetActivity : BaseActivity() {
@ -33,10 +34,10 @@ class NoInternetActivity : BaseActivity() {
}
binding.downloads.setOnClickListener {
supportFragmentManager.beginTransaction()
.replace(R.id.noInternet_container, DownloadsFragment())
.addToBackStack(null)
.commit()
supportFragmentManager.commit {
replace<DownloadsFragment>(R.id.noInternet_container)
addToBackStack(null)
}
}
setContentView(binding.root)

View File

@ -16,12 +16,12 @@ import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.helpers.WindowHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setAspectRatio
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.WindowHelper
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem

View File

@ -7,8 +7,8 @@ import android.os.Bundle
import android.util.Log
import com.github.libretube.constants.IntentData
import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.TextUtils
class RouterActivity : BaseActivity() {

View File

@ -24,10 +24,9 @@ class SettingsActivity : BaseActivity() {
}
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainSettings())
.commit()
supportFragmentManager.commit {
replace<MainSettings>(R.id.settings)
}
}
// new way of dealing with back presses instead of onBackPressed()

View File

@ -1,11 +1,11 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.BottomSheetItemBinding
import com.github.libretube.obj.BottomSheetItem
import com.github.libretube.ui.extensions.setDrawables
import com.github.libretube.ui.viewholders.BottomSheetViewHolder
class BottomSheetAdapter(
@ -23,17 +23,12 @@ class BottomSheetAdapter(
override fun onBindViewHolder(holder: BottomSheetViewHolder, position: Int) {
val item = items[position]
holder.binding.apply {
holder.binding.root.apply {
val current = item.getCurrent()
title.text =
if (current != null) "${item.title} ($current)" else item.title
if (item.drawable != null) {
drawable.setImageResource(item.drawable)
} else {
drawable.visibility = View.GONE
}
text = if (current != null) "${item.title} ($current)" else item.title
setDrawables(start = item.drawable)
root.setOnClickListener {
setOnClickListener {
item.onClick.invoke()
listener.invoke(position)
}

View File

@ -7,9 +7,9 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.databinding.ChapterColumnBinding
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.viewholders.ChaptersViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.exoplayer2.ExoPlayer
class ChaptersAdapter(

View File

@ -1,17 +1,19 @@
package com.github.libretube.ui.adapters
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
@ -19,13 +21,14 @@ import com.github.libretube.api.obj.Comment
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.CommentsRowBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.helpers.ClipboardHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.fragments.CommentsRepliesFragment
import com.github.libretube.ui.viewholders.CommentsViewHolder
import com.github.libretube.util.ClipboardHelper
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.TextUtils
import com.github.libretube.util.ThemeHelper
import kotlinx.serialization.encodeToString
class CommentsAdapter(
private val fragment: Fragment?,
@ -65,7 +68,7 @@ class CommentsAdapter(
if (comment.verified) verifiedImageView.visibility = View.VISIBLE
if (comment.pinned) pinnedImageView.visibility = View.VISIBLE
if (comment.hearted) heartedImageView.visibility = View.VISIBLE
if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE
if (comment.repliesPage != null) repliesCount.visibility = View.VISIBLE
if (comment.replyCount > 0L) {
repliesCount.text = comment.replyCount.formatShort()
}
@ -77,7 +80,6 @@ class CommentsAdapter(
if (isRepliesAdapter) {
repliesCount.visibility = View.GONE
repliesAvailable.visibility = View.GONE
// highlight the comment that is being replied to
if (comment == comments.firstOrNull()) {
@ -87,31 +89,28 @@ class CommentsAdapter(
root.updatePadding(top = 20)
root.updateLayoutParams<MarginLayoutParams> { bottomMargin = 20 }
} else {
root.background = AppCompatResources.getDrawable(root.context, R.drawable.rounded_ripple)
root.background = AppCompatResources.getDrawable(
root.context,
R.drawable.rounded_ripple
)
}
}
if (!isRepliesAdapter && comment.repliesPage != null) {
val repliesFragment = CommentsRepliesFragment().apply {
arguments = Bundle().apply {
putString(IntentData.videoId, videoId)
putString(
IntentData.comment,
JsonHelper.json.encodeToString(Comment.serializer(), comment)
)
}
}
root.setOnClickListener {
fragment!!.parentFragmentManager
.beginTransaction()
.replace(R.id.commentFragContainer, repliesFragment)
.addToBackStack(null)
.commit()
val args = bundleOf(
IntentData.videoId to videoId,
IntentData.comment to JsonHelper.json.encodeToString(comment)
)
fragment!!.parentFragmentManager.commit {
replace<CommentsRepliesFragment>(R.id.commentFragContainer, args = args)
addToBackStack(null)
}
}
}
root.setOnLongClickListener {
ClipboardHelper(root.context).save(comment.commentText ?: "")
ClipboardHelper.save(root.context, comment.commentText ?: "")
Toast.makeText(root.context, R.string.copied, Toast.LENGTH_SHORT).show()
true
}

View File

@ -14,9 +14,9 @@ import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.extensions.query
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.github.libretube.util.ImageHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File

View File

@ -8,9 +8,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.AppIconItemBinding
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.viewholders.IconsSheetViewHolder
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
class IconsSheetAdapter : RecyclerView.Adapter<IconsSheetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IconsSheetViewHolder {

View File

@ -5,11 +5,11 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.LegacySubscriptionChannelBinding
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.viewholders.LegacySubscriptionViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class LegacySubscriptionAdapter(
private val subscriptions: List<com.github.libretube.api.obj.Subscription>

View File

@ -8,10 +8,10 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.QueueRowBinding
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.viewholders.PlayingQueueViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.ThemeHelper
class PlayingQueueAdapter : RecyclerView.Adapter<PlayingQueueViewHolder>() {

View File

@ -13,13 +13,13 @@ import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import java.io.IOException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -84,9 +84,7 @@ class PlaylistAdapter(
if (!streamItem.uploaderUrl.isNullOrBlank()) {
channelContainer.setOnClickListener {
streamItem.uploaderUrl?.toID()?.let {
NavigationHelper.navigateChannel(root.context, it)
}
NavigationHelper.navigateChannel(root.context, streamItem.uploaderUrl.toID())
}
}
@ -96,7 +94,7 @@ class PlaylistAdapter(
removeFromPlaylist(root.context, position)
}
}
watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
watchProgress.setWatchProgressLength(videoId, streamItem.duration)
}
}

View File

@ -12,10 +12,10 @@ import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.query
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistBookmarkViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class PlaylistBookmarkAdapter(
private val bookmarks: List<PlaylistBookmark>,

View File

@ -7,12 +7,12 @@ import com.github.libretube.R
import com.github.libretube.api.obj.Playlists
import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.DeletePlaylistDialog
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistsViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class PlaylistsAdapter(
private val playlists: MutableList<Playlists>,

View File

@ -4,7 +4,8 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R
import com.github.libretube.api.obj.ContentItem
import com.github.libretube.databinding.ChannelRowBinding
@ -13,6 +14,8 @@ import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
@ -21,25 +24,9 @@ import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.SearchViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.TextUtils
class SearchAdapter(
private val searchItems: MutableList<ContentItem>
) :
RecyclerView.Adapter<SearchViewHolder>() {
fun updateItems(newItems: List<ContentItem>) {
val searchItemsSize = searchItems.size
searchItems.addAll(newItems)
notifyItemRangeInserted(searchItemsSize, newItems.size)
}
override fun getItemCount(): Int {
return searchItems.size
}
class SearchAdapter : ListAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
@ -58,7 +45,7 @@ class SearchAdapter(
}
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
val searchItem = searchItems[position]
val searchItem = currentList[position]
val videoRowBinding = holder.videoRowBinding
val channelRowBinding = holder.channelRowBinding
@ -68,11 +55,13 @@ class SearchAdapter(
bindWatch(searchItem, videoRowBinding)
} else if (channelRowBinding != null) {
bindChannel(searchItem, channelRowBinding)
} else if (playlistRowBinding != null) bindPlaylist(searchItem, playlistRowBinding)
} else if (playlistRowBinding != null) {
bindPlaylist(searchItem, playlistRowBinding)
}
}
override fun getItemViewType(position: Int): Int {
return when (searchItems[position].type) {
return when (currentList[position].type) {
"stream" -> 0
"channel" -> 1
"playlist" -> 2
@ -116,10 +105,7 @@ class SearchAdapter(
}
@SuppressLint("SetTextI18n")
private fun bindChannel(
item: ContentItem,
binding: ChannelRowBinding
) {
private fun bindChannel(item: ContentItem, binding: ChannelRowBinding) {
binding.apply {
ImageHelper.loadImage(item.thumbnail, searchChannelImage)
searchChannelName.text = item.name
@ -144,10 +130,7 @@ class SearchAdapter(
}
}
private fun bindPlaylist(
item: ContentItem,
binding: PlaylistsRowBinding
) {
private fun bindPlaylist(item: ContentItem, binding: PlaylistsRowBinding) {
binding.apply {
ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
if (item.videos != -1L) videoCount.text = item.videos.toString()
@ -169,4 +152,14 @@ class SearchAdapter(
}
}
}
private object SearchCallback : DiffUtil.ItemCallback<ContentItem>() {
override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
return true
}
}
}

View File

@ -6,12 +6,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.api.obj.Subscription
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class SubscriptionChannelAdapter(
private val subscriptions: MutableList<Subscription>

View File

@ -20,14 +20,14 @@ import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.extensions.dpToPx
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.VideosViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.TextUtils
class VideosAdapter(

View File

@ -8,13 +8,13 @@ import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.extensions.query
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.WatchHistoryViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class WatchHistoryAdapter(
private val watchHistory: MutableList<WatchHistoryItem>

View File

@ -2,8 +2,8 @@ package com.github.libretube.ui.base
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.ThemeHelper
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.helpers.ThemeHelper
/**
* Activity that applies the LibreTube theme and the in-app language

View File

@ -7,8 +7,8 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.github.libretube.R
import com.github.libretube.databinding.DialogTextPreferenceBinding
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**

View File

@ -7,9 +7,9 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@ -7,6 +7,7 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.databinding.DialogCreatePlaylistBinding
import com.github.libretube.extensions.toastFromMainThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -24,8 +25,19 @@ class CreatePlaylistDialog(
binding.clonePlaylist.setOnClickListener {
val playlistUrl = binding.playlistUrl.text.toString().toHttpUrlOrNull()
val appContext = context?.applicationContext
playlistUrl?.queryParameter("list")?.let {
PlaylistsHelper.clonePlaylist(requireContext(), it)
CoroutineScope(Dispatchers.IO).launch {
val playlistId = PlaylistsHelper.clonePlaylist(requireContext(), it)?.also {
withContext(Dispatchers.Main) {
onSuccess.invoke()
}
}
appContext?.toastFromMainThread(
if (playlistId != null) R.string.playlistCloned else R.string.server_error
)
}
dismiss()
} ?: run {
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show()

View File

@ -12,7 +12,7 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.DeleteUserRequest
import com.github.libretube.databinding.DialogDeleteAccountBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class DeleteAccountDialog : DialogFragment() {

View File

@ -4,16 +4,15 @@ import android.app.Dialog
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -28,44 +27,41 @@ class DeletePlaylistDialog(
.setTitle(R.string.deletePlaylist)
.setMessage(R.string.areYouSure)
.setPositiveButton(R.string.yes) { _, _ ->
PreferenceHelper.getToken()
deletePlaylist()
lifecycleScope.launch(Dispatchers.IO) {
deletePlaylist()
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun deletePlaylist() {
private suspend fun deletePlaylist() {
if (playlistType == PlaylistType.LOCAL) {
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(
playlistId
)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId)
withContext(Dispatchers.Main) {
onSuccess()
}
onSuccess.invoke()
return
}
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.deletePlaylist(
PreferenceHelper.getToken(),
PlaylistId(playlistId)
)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launch
}
try {
if (response.message == "ok") {
withContext(Dispatchers.Main) {
onSuccess.invoke()
}
val response = try {
RetrofitInstance.authApi.deletePlaylist(
PreferenceHelper.getToken(),
PlaylistId(playlistId)
)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return
}
try {
if (response.message == "ok") {
withContext(Dispatchers.Main) {
onSuccess()
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
}
}
}

View File

@ -15,7 +15,7 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Streams
import com.github.libretube.databinding.DialogDownloadBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.util.DownloadHelper
import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.IOException

View File

@ -6,8 +6,8 @@ import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.util.ClipboardHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.ClipboardHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ErrorDialog : DialogFragment() {
@ -22,7 +22,7 @@ class ErrorDialog : DialogFragment() {
.setMessage(errorLog)
.setNegativeButton(R.string.okay, null)
.setPositiveButton(R.string.copy) { _, _ ->
ClipboardHelper(requireContext()).save(errorLog)
ClipboardHelper.save(requireContext(), errorLog)
Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
}
.show()

View File

@ -12,7 +12,7 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Login
import com.github.libretube.databinding.DialogLoginBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder

View File

@ -8,7 +8,7 @@ import androidx.core.app.ActivityCompat
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.databinding.DialogLogoutBinding
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class LogoutDialog : DialogFragment() {

View File

@ -8,8 +8,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.databinding.DialogNavbarOptionsBinding
import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.ui.adapters.NavBarOptionsAdapter
import com.github.libretube.util.NavBarHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class NavBarOptionsDialog : DialogFragment() {

View File

@ -5,7 +5,7 @@ import android.os.Bundle
import androidx.core.app.ActivityCompat
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.util.NavigationHelper
import com.github.libretube.helpers.NavigationHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class RequireRestartDialog : DialogFragment() {

View File

@ -13,8 +13,8 @@ import com.github.libretube.databinding.DialogShareBinding
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.ShareData
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ShareDialog(

View File

@ -0,0 +1,18 @@
package com.github.libretube.ui.extensions
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
fun TextView.setDrawables(
start: Int? = null,
top: Int? = null,
end: Int? = null,
bottom: Int? = null
) {
setCompoundDrawablesRelativeWithIntrinsicBounds(
start?.let { AppCompatResources.getDrawable(context, it) },
top?.let { AppCompatResources.getDrawable(context, it) },
end?.let { AppCompatResources.getDrawable(context, it) },
bottom?.let { AppCompatResources.getDrawable(context, it) }
)
}

View File

@ -1,8 +1,7 @@
package com.github.libretube.ui.extensions
import android.view.View
import android.view.ViewTreeObserver
import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updateLayoutParams
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.awaitQuery
@ -13,8 +12,11 @@ import com.github.libretube.extensions.awaitQuery
* @param duration The duration of the video in seconds
* @return Whether the video is already watched more than 90%
*/
fun View?.setWatchProgressLength(videoId: String, duration: Long): Boolean {
val view = this!!
fun View.setWatchProgressLength(videoId: String, duration: Long): Boolean {
updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = 0f
}
visibility = View.GONE
val progress = try {
awaitQuery {
@ -26,22 +28,13 @@ fun View?.setWatchProgressLength(videoId: String, duration: Long): Boolean {
?.toFloat()?.div(1000)
if (progress == null || duration == 0L) {
view.visibility = View.GONE
return false
}
view.viewTreeObserver
.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
this@setWatchProgressLength.viewTreeObserver.removeOnGlobalLayoutListener(this)
val fullWidth = (parent as LinearLayout).width
val newWidth = fullWidth * (progress / duration.toFloat())
view.updateLayoutParams {
width = newWidth.toInt()
}
view.visibility = View.VISIBLE
}
})
updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = progress / duration.toFloat()
}
visibility = View.VISIBLE
return progress / duration.toFloat() > 0.9
}

View File

@ -1,7 +1,7 @@
package com.github.libretube.ui.extensions
import com.github.libretube.R
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.button.MaterialButton
fun MaterialButton.setupNotificationBell(channelId: String) {

View File

@ -1,5 +1,6 @@
package com.github.libretube.ui.fragments
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -17,22 +18,28 @@ import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.databinding.FragmentAudioPlayerBinding
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.normalize
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.AudioHelper
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.obj.ShareData
import com.github.libretube.services.BackgroundMode
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.interfaces.AudioPlayerOptions
import com.github.libretube.ui.listeners.AudioPlayerThumbnailListener
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PlayingQueue
class AudioPlayerFragment : BaseFragment() {
class AudioPlayerFragment : BaseFragment(), AudioPlayerOptions {
private lateinit var binding: FragmentAudioPlayerBinding
private lateinit var audioHelper: AudioHelper
private val onTrackChangeListener: (StreamItem) -> Unit = {
updateStreamInfo()
}
@ -64,6 +71,7 @@ class AudioPlayerFragment : BaseFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
audioHelper = AudioHelper(requireContext())
Intent(activity, BackgroundMode::class.java).also { intent ->
activity?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
@ -78,6 +86,7 @@ class AudioPlayerFragment : BaseFragment() {
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -129,13 +138,8 @@ class AudioPlayerFragment : BaseFragment() {
).show(childFragmentManager, null)
}
binding.thumbnail.setOnClickListener {
val current = PlayingQueue.getCurrent()
current?.let {
VideoOptionsBottomSheet(it.url!!.toID(), it.title!!)
.show(childFragmentManager)
}
}
val listener = AudioPlayerThumbnailListener(requireContext(), this)
binding.thumbnail.setOnTouchListener(listener)
// Listen for track changes due to autoplay or the notification
PlayingQueue.addOnTrackChangedListener(onTrackChangeListener)
@ -146,6 +150,11 @@ class AudioPlayerFragment : BaseFragment() {
// load the stream info into the UI
updateStreamInfo()
// update the currently shown volume
binding.volumeProgressBar.let { bar ->
bar.progress = audioHelper.getVolumeWithScale(bar.max)
}
}
/**
@ -232,4 +241,48 @@ class AudioPlayerFragment : BaseFragment() {
super.onDestroy()
}
override fun onSingleTap() {
if (isPaused) playerService?.play() else playerService?.pause()
}
override fun onLongTap() {
val current = PlayingQueue.getCurrent()
VideoOptionsBottomSheet(current?.url?.toID() ?: return, current.title ?: return)
.show(childFragmentManager)
}
override fun onSwipe(distanceY: Float) {
binding.volumeControls.visibility = View.VISIBLE
updateVolume(distanceY)
}
override fun onSwipeEnd() {
binding.volumeControls.visibility = View.GONE
}
private fun updateVolume(distance: Float) {
val bar = binding.volumeProgressBar
binding.volumeControls.apply {
if (visibility == View.GONE) {
visibility = View.VISIBLE
// Volume could be changed using other mediums, sync progress
// bar with new value.
bar.progress = audioHelper.getVolumeWithScale(bar.max)
}
}
if (bar.progress == 0) {
binding.volumeImageView.setImageResource(
when {
distance > 0 -> R.drawable.ic_volume_up
else -> R.drawable.ic_volume_off
}
)
}
bar.incrementProgressBy(distance.toInt() / 3)
audioHelper.setVolumeWithScale(bar.progress, bar.max)
binding.volumeTextView.text = "${bar.progress.normalize(0, bar.max, 0, 100)}"
}
}

View File

@ -19,6 +19,7 @@ import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.obj.ChannelTabs
import com.github.libretube.obj.ShareData
import com.github.libretube.ui.adapters.SearchAdapter
@ -26,11 +27,10 @@ import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.util.ImageHelper
import java.io.IOException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
class ChannelFragment : BaseFragment() {
@ -45,8 +45,6 @@ class ChannelFragment : BaseFragment() {
private var onScrollEnd: () -> Unit = {}
private val scope = CoroutineScope(Dispatchers.IO)
val possibleTabs = listOf(
ChannelTabs.Channels,
ChannelTabs.Playlists,
@ -227,18 +225,18 @@ class ChannelFragment : BaseFragment() {
}
private fun loadTab(tab: ChannelTab) {
scope.launch {
lifecycleScope.launch {
val response = try {
RetrofitInstance.api.getChannelTab(tab.data)
withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelTab(tab.data)
}
} catch (e: Exception) {
return@launch
}
val adapter = SearchAdapter(response.content.toMutableList())
runOnUiThread {
binding.channelRecView.adapter = adapter
}
val adapter = SearchAdapter()
binding.channelRecView.adapter = adapter
adapter.submitList(response.content)
var tabNextPage = response.nextpage
onScrollEnd = {
@ -284,18 +282,18 @@ class ChannelFragment : BaseFragment() {
adapter: SearchAdapter,
onNewNextPage: (String?) -> Unit
) {
scope.launch {
lifecycleScope.launch {
val newContent = try {
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
}
} catch (e: Exception) {
Log.e(TAG(), "Exception: $e")
null
}
onNewNextPage.invoke(newContent?.nextpage)
runOnUiThread {
newContent?.content?.let {
adapter.updateItems(it)
}
onNewNextPage(newContent?.nextpage)
newContent?.content?.let {
adapter.submitList(adapter.currentList + it)
}
}
}

View File

@ -58,7 +58,7 @@ class CommentsMainFragment : Fragment() {
viewModel.commentsPage.observe(viewLifecycleOwner) {
it ?: return@observe
binding.progress.visibility = View.GONE
if (it.disabled == true) {
if (it.disabled) {
binding.errorTV.visibility = View.VISIBLE
return@observe
}

View File

@ -19,13 +19,13 @@ import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.obj.DownloadStatus
import com.github.libretube.receivers.DownloadReceiver
import com.github.libretube.services.DownloadService
import com.github.libretube.ui.adapters.DownloadsAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.github.libretube.util.DownloadHelper
import java.io.File
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest

View File

@ -18,11 +18,11 @@ import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
import com.github.libretube.ui.adapters.PlaylistsAdapter
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.util.LocaleHelper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@ -21,13 +21,13 @@ import com.github.libretube.db.DatabaseHolder
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.dpToPx
import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
import com.github.libretube.ui.adapters.PlaylistsAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.CreatePlaylistDialog
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.NavBarHelper
import com.github.libretube.util.PreferenceHelper
class LibraryFragment : BaseFragment() {

View File

@ -35,6 +35,7 @@ import androidx.core.os.postDelayed
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
@ -67,6 +68,13 @@ import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.DashHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.ShareData
import com.github.libretube.obj.VideoResolution
import com.github.libretube.services.BackgroundMode
@ -81,24 +89,17 @@ import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.setAspectRatio
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.listeners.SeekbarPreviewListener
import com.github.libretube.ui.models.CommentsViewModel
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.CommentsSheet
import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.DashHelper
import com.github.libretube.util.DataSaverMode
import com.github.libretube.util.HtmlParser
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.LinkHandler
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.SeekbarPreviewListener
import com.github.libretube.util.TextUtils
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
@ -1510,9 +1511,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
private fun killPlayerFragment() {
viewModel.isFullscreen.value = false
binding.playerMotionLayout.transitionToEnd()
mainActivity.supportFragmentManager.beginTransaction()
.remove(this)
.commit()
mainActivity.supportFragmentManager.commit {
remove(this@PlayerFragment)
}
onDestroy()
}

View File

@ -26,13 +26,13 @@ import com.github.libretube.extensions.dpToPx
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toPlaylistBookmark
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.adapters.PlaylistAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.extensions.serializable
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.TextUtils

View File

@ -16,9 +16,11 @@ import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.hideKeyboard
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.util.PreferenceHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import retrofit2.HttpException
@ -85,7 +87,9 @@ class SearchResultFragment : BaseFragment() {
lifecycleScope.launchWhenCreated {
view?.let { context?.hideKeyboard(it) }
val response = try {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
}
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection $e")
@ -94,11 +98,10 @@ class SearchResultFragment : BaseFragment() {
Log.e(TAG(), "HttpException, unexpected response")
return@launchWhenCreated
}
runOnUiThread {
searchAdapter = SearchAdapter(response.items.toMutableList())
binding.searchRecycler.adapter = searchAdapter
binding.noSearchResult.isVisible = response.items.isEmpty()
}
searchAdapter = SearchAdapter()
binding.searchRecycler.adapter = searchAdapter
searchAdapter.submitList(response.items)
binding.noSearchResult.isVisible = response.items.isEmpty()
nextPage = response.nextpage
}
}
@ -106,11 +109,13 @@ class SearchResultFragment : BaseFragment() {
private fun fetchNextSearchItems() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getSearchResultsNextPage(
query,
apiSearchFilter,
nextPage!!
)
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResultsNextPage(
query,
apiSearchFilter,
nextPage!!
)
}
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
@ -120,10 +125,8 @@ class SearchResultFragment : BaseFragment() {
return@launchWhenCreated
}
nextPage = response.nextpage!!
kotlin.runCatching {
if (response.items.isNotEmpty()) {
searchAdapter.updateItems(response.items)
}
if (response.items.isNotEmpty()) {
searchAdapter.submitList(searchAdapter.currentList + response.items)
}
}
}

View File

@ -13,13 +13,13 @@ import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentSubscriptionsBinding
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.LegacySubscriptionAdapter
import com.github.libretube.ui.adapters.SubscriptionChannelAdapter
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.util.PreferenceHelper
class SubscriptionsFragment : BaseFragment() {
private lateinit var binding: FragmentSubscriptionsBinding

View File

@ -12,10 +12,10 @@ import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.FragmentTrendsBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.util.LocaleHelper
import com.google.android.material.snackbar.Snackbar
import java.io.IOException
import retrofit2.HttpException

View File

@ -19,12 +19,12 @@ import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.dpToPx
import com.github.libretube.extensions.query
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.ui.adapters.WatchHistoryAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.ProxyHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class WatchHistoryFragment : BaseFragment() {

View File

@ -0,0 +1,12 @@
package com.github.libretube.ui.interfaces
interface AudioPlayerOptions {
fun onSingleTap()
fun onLongTap()
fun onSwipe(distanceY: Float)
fun onSwipeEnd()
}

View File

@ -0,0 +1,81 @@
package com.github.libretube.ui.listeners
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.os.postDelayed
import com.github.libretube.ui.interfaces.AudioPlayerOptions
import kotlin.math.abs
class AudioPlayerThumbnailListener(context: Context, private val listener: AudioPlayerOptions) :
View.OnTouchListener {
private val handler = Handler(Looper.getMainLooper())
private val gestureDetector = GestureDetector(context, GestureListener(), handler)
private var isMoving = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP && isMoving) {
isMoving = false
listener.onSwipeEnd()
return false
}
runCatching {
gestureDetector.onTouchEvent(event)
}
return true
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
if (isMoving) return false
handler.postDelayed(ACTION_INTERVAL, SINGLE_PRESS_TOKEN) {
if (!isMoving) listener.onSingleTap()
}
return true
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
val insideThreshHold = abs(e2.y - e1.y) <= MOVEMENT_THRESHOLD
// If the movement is inside threshold or scroll is horizontal then return false
if (!isMoving && (insideThreshHold || abs(distanceX) > abs(distanceY))) {
return false
}
isMoving = true
listener.onSwipe(distanceY)
return true
}
override fun onLongPress(e: MotionEvent) {
// remove to single press action from the queue
handler.removeCallbacksAndMessages(SINGLE_PRESS_TOKEN)
listener.onLongTap()
}
}
companion object {
private const val MOVEMENT_THRESHOLD = 10
private val ACTION_INTERVAL = ViewConfiguration.getLongPressTimeout().toLong()
private const val SINGLE_PRESS_TOKEN = "singlePress"
}
}

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.ui.listeners
import android.annotation.SuppressLint
import android.content.res.Configuration
@ -12,6 +12,7 @@ import android.view.ScaleGestureDetector
import android.view.View
import androidx.activity.viewModels
import androidx.core.os.postDelayed
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.interfaces.PlayerGestureOptions
import com.github.libretube.ui.models.PlayerViewModel

View File

@ -1,4 +1,4 @@
package com.github.libretube.util
package com.github.libretube.ui.listeners
import android.graphics.Bitmap
import android.view.View
@ -9,6 +9,7 @@ import androidx.core.math.MathUtils
import androidx.core.view.updateLayoutParams
import coil.request.ImageRequest
import com.github.libretube.api.obj.PreviewFrames
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.obj.PreviewFrame
import com.google.android.exoplayer2.ui.TimeBar

View File

@ -9,7 +9,7 @@ import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Subscription
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@ -6,9 +6,9 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class AdvancedSettings : BasePreferenceFragment() {

View File

@ -7,12 +7,12 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.IconsSheetAdapter
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.NavBarOptionsDialog
import com.github.libretube.ui.dialogs.RequireRestartDialog
import com.github.libretube.ui.sheets.IconsBottomSheet
import com.github.libretube.util.PreferenceHelper
class AppearanceSettings : BasePreferenceFragment() {
override val titleResourceId: Int = R.string.appearance

View File

@ -1,79 +1,79 @@
package com.github.libretube.ui.preferences
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.github.libretube.R
import com.github.libretube.helpers.BackupHelper
import com.github.libretube.helpers.ImportHelper
import com.github.libretube.obj.BackupFile
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.BackupDialog
import com.github.libretube.util.BackupHelper
import com.github.libretube.util.ImportHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class BackupRestoreSettings : BasePreferenceFragment() {
private val backupDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss")
private var backupFile = BackupFile()
override val titleResourceId: Int = R.string.backup_restore
// backup and restore database
private lateinit var getBackupFile: ActivityResultLauncher<String>
private lateinit var createBackupFile: ActivityResultLauncher<String>
private var backupFile = BackupFile()
private val getBackupFile = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
BackupHelper.restoreAdvancedBackup(requireContext(), it)
}
}
}
private val createBackupFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
BackupHelper.createAdvancedBackup(requireContext(), it, backupFile)
}
}
}
/**
* result listeners for importing and exporting subscriptions
*/
private lateinit var getSubscriptionsFile: ActivityResultLauncher<String>
private lateinit var createSubscriptionsFile: ActivityResultLauncher<String>
private val getSubscriptionsFile = registerForActivityResult(
ActivityResultContracts.GetContent()
) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.importSubscriptions(requireActivity(), it)
}
}
}
private val createSubscriptionsFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportSubscriptions(requireActivity(), it)
}
}
}
/**
* result listeners for importing and exporting playlists
*/
private lateinit var getPlaylistsFile: ActivityResultLauncher<String>
private lateinit var createPlaylistsFile: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
getSubscriptionsFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
ImportHelper(requireActivity()).importSubscriptions(uri)
private val getPlaylistsFile = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.importPlaylists(requireActivity(), it)
}
createSubscriptionsFile = registerForActivityResult(
CreateDocument("application/json")
) { uri ->
ImportHelper(requireActivity()).exportSubscriptions(uri)
}
getPlaylistsFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
ImportHelper(requireActivity()).importPlaylists(uri)
}
createPlaylistsFile = registerForActivityResult(
CreateDocument("application/json")
) { uri ->
ImportHelper(requireActivity()).exportPlaylists(uri)
}
getBackupFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
BackupHelper(requireContext()).restoreAdvancedBackup(uri)
}
private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportPlaylists(requireActivity(), it)
}
createBackupFile = registerForActivityResult(
CreateDocument("application/json")
) { uri: Uri? ->
BackupHelper(requireContext()).createAdvancedBackup(uri, backupFile)
}
super.onCreate(savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -116,8 +116,12 @@ class BackupRestoreSettings : BasePreferenceFragment() {
val restoreAdvancedBackup = findPreference<Preference>("restore")
restoreAdvancedBackup?.setOnPreferenceClickListener {
getBackupFile.launch("application/json")
getBackupFile.launch(JSON)
true
}
}
companion object {
private const val JSON = "application/json"
}
}

View File

@ -7,10 +7,10 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.RequireRestartDialog
import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.PreferenceHelper
class GeneralSettings : BasePreferenceFragment() {
override val titleResourceId: Int = R.string.general

View File

@ -15,12 +15,12 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.CustomInstanceDialog
import com.github.libretube.ui.dialogs.DeleteAccountDialog
import com.github.libretube.ui.dialogs.LoginDialog
import com.github.libretube.ui.dialogs.LogoutDialog
import com.github.libretube.util.PreferenceHelper
class InstanceSettings : BasePreferenceFragment() {
override val titleResourceId: Int = R.string.instance

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.commitNow
import androidx.preference.Preference
import com.github.libretube.BuildConfig
import com.github.libretube.R
@ -124,9 +125,9 @@ class MainSettings : BasePreferenceFragment() {
}
private fun navigateToSettingsFragment(newFragment: Fragment): Boolean {
parentFragmentManager.beginTransaction()
.replace(R.id.settings, newFragment)
.commitNow()
parentFragmentManager.commitNow {
replace(R.id.settings, newFragment)
}
return true
}
}

View File

@ -6,9 +6,9 @@ import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingPeriodicWorkPolicy
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.NotificationHelper
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.views.TimePickerPreference
import com.github.libretube.util.NotificationHelper
class NotificationSettings : BasePreferenceFragment() {
override val titleResourceId: Int = R.string.notifications

View File

@ -12,9 +12,9 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.PreferenceHelper
class PlayerSettings : BasePreferenceFragment() {
override val titleResourceId: Int = R.string.player

View File

@ -7,10 +7,10 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.obj.ShareData
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.NavigationHelper
import kotlinx.coroutines.runBlocking
/**

View File

@ -7,7 +7,7 @@ import android.view.ViewGroup
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.PlaybackBottomSheetBinding
import com.github.libretube.extensions.round
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters

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