Introduce klint (#186)

* Add ktlint plugin

* Make code ktlint compliant

* Make code ktlint compliant
This commit is contained in:
Jarne Demeulemeester 2022-10-29 21:17:48 +02:00 committed by GitHub
parent 45ccea57af
commit ad5e722d44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
105 changed files with 682 additions and 524 deletions

View file

@ -6,6 +6,7 @@ plugins {
id("androidx.navigation.safeargs.kotlin") id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
} }
android { android {
@ -59,6 +60,12 @@ android {
} }
} }
ktlint {
android.set(true)
ignoreFailures.set(false)
disabledRules.add("max-line-length")
}
dependencies { dependencies {
implementation("androidx.leanback:leanback:1.2.0-alpha02") implementation("androidx.leanback:leanback:1.2.0-alpha02")
@ -67,7 +74,6 @@ dependencies {
implementation("androidx.activity:activity-ktx:1.6.1") implementation("androidx.activity:activity-ktx:1.6.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// Material // Material
implementation("com.google.android.material:material:1.7.0") implementation("com.google.android.material:material:1.7.0")
@ -131,4 +137,4 @@ dependencies {
val pagingVersion = "3.1.1" val pagingVersion = "3.1.1"
implementation("androidx.paging:paging-runtime-ktx:$pagingVersion") implementation("androidx.paging:paging-runtime-ktx:$pagingVersion")
} }

View file

@ -27,4 +27,4 @@ class BaseApplication : Application() {
if (appPreferences.dynamicColors) DynamicColors.applyToActivitiesIfAvailable(this) if (appPreferences.dynamicColors) DynamicColors.applyToActivitiesIfAvailable(this)
} }
} }

View file

@ -8,7 +8,7 @@ import androidx.core.view.updatePadding
import com.google.android.exoplayer2.trackselection.MappingTrackSelector import com.google.android.exoplayer2.trackselection.MappingTrackSelector
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
abstract class BasePlayerActivity: AppCompatActivity() { abstract class BasePlayerActivity : AppCompatActivity() {
abstract val viewModel: PlayerActivityViewModel abstract val viewModel: PlayerActivityViewModel
@ -27,9 +27,11 @@ abstract class BasePlayerActivity: AppCompatActivity() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
protected fun hideSystemUI() { protected fun hideSystemUI() {
// These methods are deprecated but we still use them because the new WindowInsetsControllerCompat has a bug which makes the action bar reappear // These methods are deprecated but we still use them because the new WindowInsetsControllerCompat has a bug which makes the action bar reappear
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@ -66,4 +68,4 @@ abstract class BasePlayerActivity: AppCompatActivity() {
} }
} }
} }
} }

View file

@ -13,11 +13,11 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.models.User import dev.jdtech.jellyfin.models.User
import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID
@BindingAdapter("servers") @BindingAdapter("servers")
fun bindServers(recyclerView: RecyclerView, data: List<Server>?) { fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
@ -75,7 +75,7 @@ fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
var imageItemId = episode.id var imageItemId = episode.id
var imageType = ImageType.PRIMARY var imageType = ImageType.PRIMARY
if (!episode.imageTags.isNullOrEmpty()) { //TODO: Downloadmetadata currently does not store imagetags, so it always uses the backdrop if (!episode.imageTags.isNullOrEmpty()) { // TODO: Downloadmetadata currently does not store imagetags, so it always uses the backdrop
when (episode.type) { when (episode.type) {
BaseItemKind.MOVIE -> { BaseItemKind.MOVIE -> {
if (!episode.backdropImageTags.isNullOrEmpty()) { if (!episode.backdropImageTags.isNullOrEmpty()) {
@ -96,13 +96,13 @@ fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
} }
imageView imageView
.loadImage("/items/${imageItemId}/Images/$imageType") .loadImage("/items/$imageItemId/Images/$imageType")
.posterDescription(episode.name) .posterDescription(episode.name)
} }
@BindingAdapter("seasonPoster") @BindingAdapter("seasonPoster")
fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) { fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
imageView.loadImage("/items/${seasonId}/Images/${ImageType.PRIMARY}") imageView.loadImage("/items/$seasonId/Images/${ImageType.PRIMARY}")
} }
@BindingAdapter("userImage") @BindingAdapter("userImage")
@ -112,7 +112,11 @@ fun bindUserImage(imageView: ImageView, user: User) {
.posterDescription(user.name) .posterDescription(user.name)
} }
private fun ImageView.loadImage(url: String, @DrawableRes placeholderId: Int = R.color.neutral_800, @DrawableRes errorPlaceHolderId: Int? = null): View { private fun ImageView.loadImage(
url: String,
@DrawableRes placeholderId: Int = R.color.neutral_800,
@DrawableRes errorPlaceHolderId: Int? = null
): View {
val api = JellyfinApi.getInstance(context.applicationContext) val api = JellyfinApi.getInstance(context.applicationContext)
Glide Glide
@ -134,4 +138,4 @@ private fun View.posterDescription(name: String?) {
private fun View.backdropDescription(name: String?) { private fun View.backdropDescription(name: String?) {
contentDescription = contentDescription =
String.format(context.resources.getString(R.string.image_description_backdrop), name) String.format(context.resources.getString(R.string.image_description_backdrop), name)
} }

View file

@ -59,7 +59,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) { if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) {
val navView: NavigationBarView = binding.navView as NavigationBarView val navView: NavigationBarView = binding.navView as NavigationBarView
@ -109,4 +108,4 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
} }

View file

@ -21,8 +21,8 @@ import dev.jdtech.jellyfin.mpv.TrackType
import dev.jdtech.jellyfin.utils.AppPreferences import dev.jdtech.jellyfin.utils.AppPreferences
import dev.jdtech.jellyfin.utils.PlayerGestureHelper import dev.jdtech.jellyfin.utils.PlayerGestureHelper
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class PlayerActivity : BasePlayerActivity() { class PlayerActivity : BasePlayerActivity() {
@ -177,4 +177,3 @@ class PlayerActivity : BasePlayerActivity() {
hideSystemUI() hideSystemUI()
} }
} }

View file

@ -50,4 +50,4 @@ class CollectionListAdapter(
class OnClickListener(val clickListener: (collection: BaseItemDto) -> Unit) { class OnClickListener(val clickListener: (collection: BaseItemDto) -> Unit) {
fun onClick(collection: BaseItemDto) = clickListener(collection) fun onClick(collection: BaseItemDto) = clickListener(collection)
} }
} }

View file

@ -9,7 +9,8 @@ import dev.jdtech.jellyfin.databinding.DiscoveredServerItemBinding
import dev.jdtech.jellyfin.models.DiscoveredServer import dev.jdtech.jellyfin.models.DiscoveredServer
class DiscoveredServerListAdapter( class DiscoveredServerListAdapter(
private val clickListener: (server: DiscoveredServer) -> Unit) : private val clickListener: (server: DiscoveredServer) -> Unit
) :
ListAdapter<DiscoveredServer, DiscoveredServerListAdapter.DiscoveredServerViewHolder>( ListAdapter<DiscoveredServer, DiscoveredServerListAdapter.DiscoveredServerViewHolder>(
DiffCallback DiffCallback
) { ) {
@ -55,4 +56,4 @@ class DiscoveredServerListAdapter(
holder.itemView.setOnClickListener { clickListener(server) } holder.itemView.setOnClickListener { clickListener(server) }
holder.bind(server) holder.bind(server)
} }
} }

View file

@ -12,8 +12,8 @@ import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.UUID import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
private const val ITEM_VIEW_TYPE_HEADER = 0 private const val ITEM_VIEW_TYPE_HEADER = 0
private const val ITEM_VIEW_TYPE_EPISODE = 1 private const val ITEM_VIEW_TYPE_EPISODE = 1
@ -125,4 +125,4 @@ sealed class DownloadEpisodeItem {
data class Episode(val episode: PlayerItem) : DownloadEpisodeItem() { data class Episode(val episode: PlayerItem) : DownloadEpisodeItem() {
override val id = episode.itemId override val id = episode.itemId
} }
} }

View file

@ -13,8 +13,7 @@ import dev.jdtech.jellyfin.utils.downloadSeriesMetadataToBaseItemDto
class DownloadSeriesListAdapter( class DownloadSeriesListAdapter(
private val onClickListener: OnClickListener, private val onClickListener: OnClickListener,
private val fixedWidth: Boolean = false, private val fixedWidth: Boolean = false,
) : ) : ListAdapter<DownloadSeriesMetadata, DownloadSeriesListAdapter.ItemViewHolder>(DiffCallback) {
ListAdapter<DownloadSeriesMetadata, DownloadSeriesListAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
@ -32,11 +31,17 @@ class DownloadSeriesListAdapter(
} }
companion object DiffCallback : DiffUtil.ItemCallback<DownloadSeriesMetadata>() { companion object DiffCallback : DiffUtil.ItemCallback<DownloadSeriesMetadata>() {
override fun areItemsTheSame(oldItem: DownloadSeriesMetadata, newItem: DownloadSeriesMetadata): Boolean { override fun areItemsTheSame(
oldItem: DownloadSeriesMetadata,
newItem: DownloadSeriesMetadata
): Boolean {
return oldItem.itemId == newItem.itemId return oldItem.itemId == newItem.itemId
} }
override fun areContentsTheSame(oldItem: DownloadSeriesMetadata, newItem: DownloadSeriesMetadata): Boolean { override fun areContentsTheSame(
oldItem: DownloadSeriesMetadata,
newItem: DownloadSeriesMetadata
): Boolean {
return oldItem == newItem return oldItem == newItem
} }
} }
@ -47,7 +52,8 @@ class DownloadSeriesListAdapter(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
), parent ),
parent
) )
} }
@ -62,4 +68,4 @@ class DownloadSeriesListAdapter(
class OnClickListener(val clickListener: (item: DownloadSeriesMetadata) -> Unit) { class OnClickListener(val clickListener: (item: DownloadSeriesMetadata) -> Unit) {
fun onClick(item: DownloadSeriesMetadata) = clickListener(item) fun onClick(item: DownloadSeriesMetadata) = clickListener(item)
} }
} }

View file

@ -14,8 +14,7 @@ import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
class DownloadViewItemListAdapter( class DownloadViewItemListAdapter(
private val onClickListener: OnClickListener, private val onClickListener: OnClickListener,
private val fixedWidth: Boolean = false, private val fixedWidth: Boolean = false,
) : ) : ListAdapter<PlayerItem, DownloadViewItemListAdapter.ItemViewHolder>(DiffCallback) {
ListAdapter<PlayerItem, DownloadViewItemListAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
@ -48,7 +47,8 @@ class DownloadViewItemListAdapter(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
), parent ),
parent
) )
} }
@ -63,4 +63,4 @@ class DownloadViewItemListAdapter(
class OnClickListener(val clickListener: (item: PlayerItem) -> Unit) { class OnClickListener(val clickListener: (item: PlayerItem) -> Unit) {
fun onClick(item: PlayerItem) = clickListener(item) fun onClick(item: PlayerItem) = clickListener(item)
} }
} }

View file

@ -61,4 +61,4 @@ class DownloadsListAdapter(
val collection = getItem(position) val collection = getItem(position)
holder.bind(collection, onClickListener, onSeriesClickListener) holder.bind(collection, onClickListener, onSeriesClickListener)
} }
} }

View file

@ -9,8 +9,8 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.EpisodeItemBinding import dev.jdtech.jellyfin.databinding.EpisodeItemBinding
import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding
import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.*
private const val ITEM_VIEW_TYPE_HEADER = 0 private const val ITEM_VIEW_TYPE_HEADER = 0
private const val ITEM_VIEW_TYPE_EPISODE = 1 private const val ITEM_VIEW_TYPE_EPISODE = 1
@ -129,4 +129,4 @@ sealed class EpisodeItem {
data class Episode(val episode: BaseItemDto) : EpisodeItem() { data class Episode(val episode: BaseItemDto) : EpisodeItem() {
override val id = episode.id override val id = episode.id
} }
} }

View file

@ -61,4 +61,4 @@ class FavoritesListAdapter(
val collection = getItem(position) val collection = getItem(position)
holder.bind(collection, onClickListener, onEpisodeClickListener) holder.bind(collection, onClickListener, onEpisodeClickListener)
} }
} }

View file

@ -17,8 +17,10 @@ class HomeEpisodeListAdapter(private val onClickListener: OnClickListener) : Lis
fun bind(episode: BaseItemDto) { fun bind(episode: BaseItemDto) {
binding.episode = episode binding.episode = episode
if (episode.userData?.playedPercentage != null) { if (episode.userData?.playedPercentage != null) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, binding.progressBar.layoutParams.width = TypedValue.applyDimension(
(episode.userData?.playedPercentage?.times(2.24))!!.toFloat(), binding.progressBar.context.resources.displayMetrics).toInt() TypedValue.COMPLEX_UNIT_DIP,
(episode.userData?.playedPercentage?.times(2.24))!!.toFloat(), binding.progressBar.context.resources.displayMetrics
).toInt()
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
} }
@ -64,4 +66,4 @@ class HomeEpisodeListAdapter(private val onClickListener: OnClickListener) : Lis
class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) {
fun onClick(item: BaseItemDto) = clickListener(item) fun onClick(item: BaseItemDto) = clickListener(item)
} }
} }

View file

@ -8,7 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.PersonItemBinding import dev.jdtech.jellyfin.databinding.PersonItemBinding
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
class PersonListAdapter(private val clickListener: (item: BaseItemPerson) -> Unit) :ListAdapter<BaseItemPerson, PersonListAdapter.PersonViewHolder>(DiffCallback) { class PersonListAdapter(private val clickListener: (item: BaseItemPerson) -> Unit) : ListAdapter<BaseItemPerson, PersonListAdapter.PersonViewHolder>(DiffCallback) {
class PersonViewHolder(private var binding: PersonItemBinding) : class PersonViewHolder(private var binding: PersonItemBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
@ -43,4 +43,4 @@ class PersonListAdapter(private val clickListener: (item: BaseItemPerson) -> Uni
holder.bind(item) holder.bind(item)
holder.itemView.setOnClickListener { clickListener(item) } holder.itemView.setOnClickListener { clickListener(item) }
} }
} }

View file

@ -11,7 +11,7 @@ import dev.jdtech.jellyfin.databinding.ServerItemBinding
class ServerGridAdapter( class ServerGridAdapter(
private val onClickListener: OnClickListener, private val onClickListener: OnClickListener,
private val onLongClickListener: OnLongClickListener private val onLongClickListener: OnLongClickListener
) : ListAdapter<Server, ServerGridAdapter.ServerViewHolder>(DiffCallback) { ) : ListAdapter<Server, ServerGridAdapter.ServerViewHolder>(DiffCallback) {
class ServerViewHolder(private var binding: ServerItemBinding) : class ServerViewHolder(private var binding: ServerItemBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(server: Server) { fun bind(server: Server) {
@ -55,4 +55,4 @@ class ServerGridAdapter(
class OnLongClickListener(val clickListener: (server: Server) -> Boolean) { class OnLongClickListener(val clickListener: (server: Server) -> Boolean) {
fun onLongClick(server: Server) = clickListener(server) fun onLongClick(server: Server) = clickListener(server)
} }
} }

View file

@ -47,4 +47,4 @@ class UserListAdapter(
holder.itemView.setOnClickListener { clickListener(user) } holder.itemView.setOnClickListener { clickListener(user) }
holder.bind(user) holder.bind(user)
} }
} }

View file

@ -14,8 +14,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
class ViewItemListAdapter( class ViewItemListAdapter(
private val onClickListener: OnClickListener, private val onClickListener: OnClickListener,
private val fixedWidth: Boolean = false, private val fixedWidth: Boolean = false,
) : ) : ListAdapter<BaseItemDto, ViewItemListAdapter.ItemViewHolder>(DiffCallback) {
ListAdapter<BaseItemDto, ViewItemListAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
@ -49,7 +48,8 @@ class ViewItemListAdapter(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
), parent ),
parent
) )
} }
@ -64,4 +64,4 @@ class ViewItemListAdapter(
class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) {
fun onClick(item: BaseItemDto) = clickListener(item) fun onClick(item: BaseItemDto) = clickListener(item)
} }
} }

View file

@ -49,7 +49,8 @@ class ViewItemPagingAdapter(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
), parent ),
parent
) )
} }
@ -66,4 +67,4 @@ class ViewItemPagingAdapter(
class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) {
fun onClick(item: BaseItemDto) = clickListener(item) fun onClick(item: BaseItemDto) = clickListener(item)
} }
} }

View file

@ -65,7 +65,8 @@ class ViewListAdapter(
NextUpSectionBinding.inflate( NextUpSectionBinding.inflate(
LayoutInflater.from( LayoutInflater.from(
parent.context parent.context
), parent, false ),
parent, false
) )
) )
ITEM_VIEW_TYPE_VIEW -> ViewViewHolder( ITEM_VIEW_TYPE_VIEW -> ViewViewHolder(
@ -119,4 +120,4 @@ sealed class HomeItem {
} }
abstract val id: UUID abstract val id: UUID
} }

View file

@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.api
import android.content.Context import android.content.Context
import dev.jdtech.jellyfin.BuildConfig import dev.jdtech.jellyfin.BuildConfig
import java.util.UUID
import org.jellyfin.sdk.api.client.extensions.devicesApi import org.jellyfin.sdk.api.client.extensions.devicesApi
import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
@ -15,7 +16,6 @@ import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.api.client.extensions.videosApi
import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.createJellyfin
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import java.util.UUID
/** /**
* Jellyfin API class using org.jellyfin.sdk:jellyfin-platform-android * Jellyfin API class using org.jellyfin.sdk:jellyfin-platform-android
@ -59,4 +59,4 @@ class JellyfinApi(androidContext: Context) {
} }
} }
} }
} }

View file

@ -1,7 +1,7 @@
package dev.jdtech.jellyfin.database package dev.jdtech.jellyfin.database
import androidx.room.TypeConverter import androidx.room.TypeConverter
import java.util.* import java.util.UUID
class Converters { class Converters {
@TypeConverter @TypeConverter
@ -13,4 +13,4 @@ class Converters {
fun fromUUIDToString(value: UUID?): String? { fun fromUUIDToString(value: UUID?): String? {
return value?.toString() return value?.toString()
} }
} }

View file

@ -13,4 +13,4 @@ import dev.jdtech.jellyfin.models.DownloadItem
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class DownloadDatabase : RoomDatabase() { abstract class DownloadDatabase : RoomDatabase() {
abstract val downloadDatabaseDao: DownloadDatabaseDao abstract val downloadDatabaseDao: DownloadDatabaseDao
} }

View file

@ -4,7 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import dev.jdtech.jellyfin.models.DownloadItem import dev.jdtech.jellyfin.models.DownloadItem
import java.util.* import java.util.UUID
@Dao @Dao
interface DownloadDatabaseDao { interface DownloadDatabaseDao {
@ -28,5 +28,4 @@ interface DownloadDatabaseDao {
@Query("SELECT EXISTS (SELECT 1 FROM downloads WHERE id = :id)") @Query("SELECT EXISTS (SELECT 1 FROM downloads WHERE id = :id)")
fun exists(id: UUID): Boolean fun exists(id: UUID): Boolean
}
}

View file

@ -12,4 +12,4 @@ data class Server(
val userId: String, val userId: String,
val userName: String, val userName: String,
val accessToken: String, val accessToken: String,
) )

View file

@ -6,4 +6,4 @@ import androidx.room.RoomDatabase
@Database(entities = [Server::class], version = 1, exportSchema = false) @Database(entities = [Server::class], version = 1, exportSchema = false)
abstract class ServerDatabase : RoomDatabase() { abstract class ServerDatabase : RoomDatabase() {
abstract val serverDatabaseDao: ServerDatabaseDao abstract val serverDatabaseDao: ServerDatabaseDao
} }

View file

@ -1,7 +1,11 @@
package dev.jdtech.jellyfin.database package dev.jdtech.jellyfin.database
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao @Dao
interface ServerDatabaseDao { interface ServerDatabaseDao {
@ -28,4 +32,4 @@ interface ServerDatabaseDao {
@Query("delete from servers where id = :id") @Query("delete from servers where id = :id")
fun delete(id: String) fun delete(id: String)
} }

View file

@ -36,4 +36,4 @@ object ApiModule {
return jellyfinApi return jellyfinApi
} }
} }

View file

@ -17,4 +17,4 @@ object AppModule {
fun provideApplication(@ApplicationContext app: Context): BaseApplication { fun provideApplication(@ApplicationContext app: Context): BaseApplication {
return app as BaseApplication return app as BaseApplication
} }
} }

View file

@ -43,4 +43,4 @@ object DatabaseModule {
.build() .build()
.downloadDatabaseDao .downloadDatabaseDao
} }
} }

View file

@ -46,4 +46,4 @@ class GlideModule : AppGlideModule() {
) )
) )
} }
} }

View file

@ -19,4 +19,4 @@ object RepositoryModule {
): JellyfinRepository { ): JellyfinRepository {
return JellyfinRepositoryImpl(jellyfinApi) return JellyfinRepositoryImpl(jellyfinApi)
} }
} }

View file

@ -10,7 +10,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object SharedPreferencesModule { object SharedPreferencesModule {

View file

@ -19,9 +19,8 @@ class DeleteServerDialogFragment(private val viewModel: ServerSelectViewModel, v
viewModel.deleteServer(server) viewModel.deleteServer(server)
} }
.setNegativeButton(getString(R.string.cancel)) { _, _ -> .setNegativeButton(getString(R.string.cancel)) { _, _ ->
} }
builder.create() builder.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View file

@ -10,7 +10,7 @@ import java.lang.IllegalStateException
class ErrorDialogFragment( class ErrorDialogFragment(
private val error: Exception private val error: Exception
) : DialogFragment() { ) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { return activity?.let {
val builder = MaterialAlertDialogBuilder(it, R.style.ErrorDialogStyle) val builder = MaterialAlertDialogBuilder(it, R.style.ErrorDialogStyle)
@ -28,9 +28,8 @@ class ErrorDialogFragment(
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent) startActivity(shareIntent)
} }
builder.create() builder.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View file

@ -8,9 +8,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
import org.jellyfin.sdk.model.api.SortOrder
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.util.* import java.util.UUID
import org.jellyfin.sdk.model.api.SortOrder
class SortDialogFragment( class SortDialogFragment(
private val parentId: UUID, private val parentId: UUID,
@ -84,4 +84,4 @@ class SortDialogFragment(
builder.create() builder.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View file

@ -29,7 +29,5 @@ class SpeedSelectionDialogFragment(
} }
builder.create() builder.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View file

@ -22,7 +22,8 @@ class TrackSelectionDialogFragment(
builder.setTitle(getString(R.string.select_audio_track)) builder.setTitle(getString(R.string.select_audio_track))
.setSingleChoiceItems( .setSingleChoiceItems(
getTrackNames(viewModel.currentAudioTracks), getTrackNames(viewModel.currentAudioTracks),
viewModel.currentAudioTracks.indexOfFirst { it.selected }) { dialog, which -> viewModel.currentAudioTracks.indexOfFirst { it.selected }
) { dialog, which ->
viewModel.switchToTrack( viewModel.switchToTrack(
TrackType.AUDIO, TrackType.AUDIO,
viewModel.currentAudioTracks[which] viewModel.currentAudioTracks[which]
@ -38,7 +39,8 @@ class TrackSelectionDialogFragment(
builder.setTitle(getString(R.string.select_subtile_track)) builder.setTitle(getString(R.string.select_subtile_track))
.setSingleChoiceItems( .setSingleChoiceItems(
getTrackNames(viewModel.currentSubtitleTracks), getTrackNames(viewModel.currentSubtitleTracks),
viewModel.currentSubtitleTracks.indexOfFirst { if (viewModel.disableSubtitle) it.ffIndex == -1 else it.selected }) { dialog, which -> viewModel.currentSubtitleTracks.indexOfFirst { if (viewModel.disableSubtitle) it.ffIndex == -1 else it.selected }
) { dialog, which ->
viewModel.switchToTrack( viewModel.switchToTrack(
TrackType.SUBTITLE, TrackType.SUBTITLE,
viewModel.currentSubtitleTracks[which] viewModel.currentSubtitleTracks[which]
@ -63,4 +65,4 @@ class TrackSelectionDialogFragment(
nameParts.joinToString(separator = " - ") nameParts.joinToString(separator = " - ")
}.toTypedArray() }.toTypedArray()
} }
} }

View file

@ -4,9 +4,9 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.IllegalStateException
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import java.lang.IllegalStateException
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
class VideoVersionDialogFragment( class VideoVersionDialogFragment(
@ -24,4 +24,4 @@ class VideoVersionDialogFragment(
}.create() }.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")
} }
} }

View file

@ -31,7 +31,8 @@ class AddServerFragment : Fragment() {
private val viewModel: AddServerViewModel by viewModels() private val viewModel: AddServerViewModel by viewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentAddServerBinding.inflate(inflater) binding = FragmentAddServerBinding.inflate(inflater)
@ -136,7 +137,9 @@ class AddServerFragment : Fragment() {
} }
} }
private fun bindDiscoveredServersStateServers(serversState: AddServerViewModel.DiscoveredServersState.Servers) { private fun bindDiscoveredServersStateServers(
serversState: AddServerViewModel.DiscoveredServersState.Servers
) {
val servers = serversState.servers val servers = serversState.servers
if (servers.isEmpty()) { if (servers.isEmpty()) {
binding.serversRecyclerView.isVisible = false binding.serversRecyclerView.isVisible = false
@ -154,4 +157,4 @@ class AddServerFragment : Fragment() {
private fun navigateToLoginFragment() { private fun navigateToLoginFragment() {
findNavController().navigate(AddServerFragmentDirections.actionAddServerFragmentToLoginFragment()) findNavController().navigate(AddServerFragmentDirections.actionAddServerFragmentToLoginFragment())
} }
} }

View file

@ -12,16 +12,18 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.adapters.* import dev.jdtech.jellyfin.adapters.DownloadSeriesListAdapter
import dev.jdtech.jellyfin.adapters.DownloadViewItemListAdapter
import dev.jdtech.jellyfin.adapters.DownloadsListAdapter
import dev.jdtech.jellyfin.databinding.FragmentDownloadBinding import dev.jdtech.jellyfin.databinding.FragmentDownloadBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.DownloadViewModel import dev.jdtech.jellyfin.viewmodels.DownloadViewModel
import java.util.UUID
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.*
@AndroidEntryPoint @AndroidEntryPoint
class DownloadFragment : Fragment() { class DownloadFragment : Fragment() {
@ -32,18 +34,21 @@ class DownloadFragment : Fragment() {
private lateinit var errorDialog: ErrorDialogFragment private lateinit var errorDialog: ErrorDialogFragment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentDownloadBinding.inflate(inflater, container, false) binding = FragmentDownloadBinding.inflate(inflater, container, false)
binding.downloadsRecyclerView.adapter = DownloadsListAdapter( binding.downloadsRecyclerView.adapter =
DownloadViewItemListAdapter.OnClickListener { item -> DownloadsListAdapter(
navigateToMediaInfoFragment(item) DownloadViewItemListAdapter.OnClickListener { item ->
}, DownloadSeriesListAdapter.OnClickListener { item -> navigateToMediaInfoFragment(item)
navigateToDownloadSeriesFragment(item) },
} DownloadSeriesListAdapter.OnClickListener { item ->
) navigateToDownloadSeriesFragment(item)
}
)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -97,11 +102,7 @@ class DownloadFragment : Fragment() {
private fun navigateToMediaInfoFragment(item: PlayerItem) { private fun navigateToMediaInfoFragment(item: PlayerItem) {
findNavController().navigate( findNavController().navigate(
DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment( DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment(
UUID.randomUUID(), UUID.randomUUID(), item.name, item.item!!.type, item, isOffline = true
item.name,
item.item!!.type,
item,
isOffline = true
) )
) )
} }
@ -109,9 +110,8 @@ class DownloadFragment : Fragment() {
private fun navigateToDownloadSeriesFragment(series: DownloadSeriesMetadata) { private fun navigateToDownloadSeriesFragment(series: DownloadSeriesMetadata) {
findNavController().navigate( findNavController().navigate(
DownloadFragmentDirections.actionDownloadFragmentToDownloadSeriesFragment( DownloadFragmentDirections.actionDownloadFragmentToDownloadSeriesFragment(
seriesMetadata = series, seriesMetadata = series, seriesName = series.name
seriesName = series.name
) )
) )
} }
} }

View file

@ -1,11 +1,11 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -18,8 +18,8 @@ import dev.jdtech.jellyfin.databinding.FragmentDownloadSeriesBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.viewmodels.DownloadSeriesViewModel import dev.jdtech.jellyfin.viewmodels.DownloadSeriesViewModel
import java.util.UUID
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
@AndroidEntryPoint @AndroidEntryPoint
class DownloadSeriesFragment : Fragment() { class DownloadSeriesFragment : Fragment() {
@ -32,7 +32,8 @@ class DownloadSeriesFragment : Fragment() {
private val args: DownloadSeriesFragmentArgs by navArgs() private val args: DownloadSeriesFragmentArgs by navArgs()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentDownloadSeriesBinding.inflate(inflater, container, false) binding = FragmentDownloadSeriesBinding.inflate(inflater, container, false)
@ -45,9 +46,12 @@ class DownloadSeriesFragment : Fragment() {
binding.viewModel = viewModel binding.viewModel = viewModel
binding.episodesRecyclerView.adapter = binding.episodesRecyclerView.adapter =
DownloadEpisodeListAdapter(DownloadEpisodeListAdapter.OnClickListener { episode -> DownloadEpisodeListAdapter(
navigateToEpisodeBottomSheetFragment(episode) DownloadEpisodeListAdapter.OnClickListener { episode ->
}, args.seriesMetadata) navigateToEpisodeBottomSheetFragment(episode)
},
args.seriesMetadata
)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -94,4 +98,4 @@ class DownloadSeriesFragment : Fragment() {
) )
) )
} }
} }

View file

@ -26,11 +26,11 @@ import dev.jdtech.jellyfin.utils.setTintColor
import dev.jdtech.jellyfin.utils.setTintColorAttribute import dev.jdtech.jellyfin.utils.setTintColorAttribute
import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import java.util.UUID
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.LocationType import org.jellyfin.sdk.model.api.LocationType
import timber.log.Timber import timber.log.Timber
import java.util.*
@AndroidEntryPoint @AndroidEntryPoint
class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
@ -50,7 +50,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.playButton.setOnClickListener { binding.playButton.setOnClickListener {
binding.playButton.setImageResource(android.R.color.transparent) binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.isVisible = true binding.progressCircular.isVisible = true
if (viewModel.canRetry){ if (viewModel.canRetry) {
binding.playButton.isEnabled = false binding.playButton.isEnabled = false
viewModel.download() viewModel.download()
return@setOnClickListener return@setOnClickListener
@ -195,7 +195,6 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
} }
} }
binding.episodeName.text = String.format( binding.episodeName.text = String.format(
getString(R.string.episode_name_extended), getString(R.string.episode_name_extended),
episode.parentIndexNumber, episode.parentIndexNumber,
@ -275,4 +274,4 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
) )
) )
} }
} }

View file

@ -1,11 +1,11 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -32,7 +32,8 @@ class FavoriteFragment : Fragment() {
private lateinit var errorDialog: ErrorDialogFragment private lateinit var errorDialog: ErrorDialogFragment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentFavoriteBinding.inflate(inflater, container, false) binding = FragmentFavoriteBinding.inflate(inflater, container, false)
@ -40,9 +41,11 @@ class FavoriteFragment : Fragment() {
binding.favoritesRecyclerView.adapter = FavoritesListAdapter( binding.favoritesRecyclerView.adapter = FavoritesListAdapter(
ViewItemListAdapter.OnClickListener { item -> ViewItemListAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item) navigateToMediaInfoFragment(item)
}, HomeEpisodeListAdapter.OnClickListener { item -> },
HomeEpisodeListAdapter.OnClickListener { item ->
navigateToEpisodeBottomSheetFragment(item) navigateToEpisodeBottomSheetFragment(item)
}) }
)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -110,4 +113,4 @@ class FavoriteFragment : Fragment() {
) )
) )
} }
} }

View file

@ -57,21 +57,24 @@ class HomeFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val menuHost: MenuHost = requireActivity() val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(object : MenuProvider { menuHost.addMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { object : MenuProvider {
menuInflater.inflate(R.menu.home_menu, menu) override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
} menuInflater.inflate(R.menu.home_menu, menu)
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_settings -> {
navigateToSettingsFragment()
true
}
else -> false
} }
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED) override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_settings -> {
navigateToSettingsFragment()
true
}
else -> false
}
}
},
viewLifecycleOwner, Lifecycle.State.RESUMED
)
} }
override fun onResume() { override fun onResume() {
@ -97,7 +100,8 @@ class HomeFragment : Fragment() {
else -> Toast.makeText(requireContext(), R.string.unknown_error, LENGTH_LONG) else -> Toast.makeText(requireContext(), R.string.unknown_error, LENGTH_LONG)
.show() .show()
} }
}) }
)
binding.errorLayout.errorRetryButton.setOnClickListener { binding.errorLayout.errorRetryButton.setOnClickListener {
viewModel.loadData() viewModel.loadData()
@ -190,4 +194,4 @@ class HomeFragment : Fragment() {
HomeFragmentDirections.actionHomeFragmentToSettingsFragment() HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
) )
} }
} }

View file

@ -4,7 +4,12 @@ import android.app.UiModeManager
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuHost import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
@ -20,18 +25,18 @@ import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearSnapHelper import androidx.recyclerview.widget.LinearSnapHelper
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
import dev.jdtech.jellyfin.adapters.ViewItemPagingAdapter import dev.jdtech.jellyfin.adapters.ViewItemPagingAdapter
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.dialogs.SortDialogFragment import dev.jdtech.jellyfin.dialogs.SortDialogFragment
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
import java.lang.IllegalArgumentException
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import java.lang.IllegalArgumentException
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class LibraryFragment : Fragment() { class LibraryFragment : Fragment() {
@ -47,7 +52,8 @@ class LibraryFragment : Fragment() {
lateinit var sp: SharedPreferences lateinit var sp: SharedPreferences
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentLibraryBinding.inflate(inflater, container, false) binding = FragmentLibraryBinding.inflate(inflater, container, false)
@ -95,7 +101,8 @@ class LibraryFragment : Fragment() {
else -> false else -> false
} }
} }
}, viewLifecycleOwner, Lifecycle.State.RESUMED },
viewLifecycleOwner, Lifecycle.State.RESUMED
) )
binding.title?.text = args.libraryName binding.title?.text = args.libraryName
@ -117,9 +124,11 @@ class LibraryFragment : Fragment() {
} }
binding.itemsRecyclerView.adapter = binding.itemsRecyclerView.adapter =
ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item -> ViewItemPagingAdapter(
navigateToMediaInfoFragment(item) ViewItemPagingAdapter.OnClickListener { item ->
}) navigateToMediaInfoFragment(item)
}
)
(binding.itemsRecyclerView.adapter as ViewItemPagingAdapter).addLoadStateListener { (binding.itemsRecyclerView.adapter as ViewItemPagingAdapter).addLoadStateListener {
when (it.refresh) { when (it.refresh) {
@ -214,4 +223,4 @@ class LibraryFragment : Fragment() {
) )
} }
} }
} }

View file

@ -31,7 +31,8 @@ class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by viewModels() private val viewModel: LoginViewModel by viewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentLoginBinding.inflate(inflater) binding = FragmentLoginBinding.inflate(inflater)
@ -165,4 +166,4 @@ class LoginFragment : Fragment() {
findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragment()) findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragment())
} }
} }
} }

View file

@ -1,7 +1,13 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuHost import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
@ -41,9 +47,11 @@ class MediaFragment : Fragment() {
binding = FragmentMediaBinding.inflate(inflater, container, false) binding = FragmentMediaBinding.inflate(inflater, container, false)
binding.viewsRecyclerView.adapter = binding.viewsRecyclerView.adapter =
CollectionListAdapter(CollectionListAdapter.OnClickListener { library -> CollectionListAdapter(
navigateToLibraryFragment(library) CollectionListAdapter.OnClickListener { library ->
}) navigateToLibraryFragment(library)
}
)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -73,32 +81,35 @@ class MediaFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val menuHost: MenuHost = requireActivity() val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(object : MenuProvider { menuHost.addMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { object : MenuProvider {
menuInflater.inflate(R.menu.media_menu, menu) override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.media_menu, menu)
val search = menu.findItem(R.id.action_search) val search = menu.findItem(R.id.action_search)
val searchView = search.actionView as SearchView val searchView = search.actionView as SearchView
searchView.queryHint = getString(R.string.search_hint) searchView.queryHint = getString(R.string.search_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(p0: String?): Boolean { override fun onQueryTextSubmit(p0: String?): Boolean {
if (p0 != null) { if (p0 != null) {
navigateToSearchResultFragment(p0) navigateToSearchResultFragment(p0)
}
return true
} }
return true
}
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
return false return false
} }
}) })
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return true return true
} }
}, viewLifecycleOwner, Lifecycle.State.RESUMED) },
viewLifecycleOwner, Lifecycle.State.RESUMED
)
} }
override fun onStart() { override fun onStart() {
@ -133,7 +144,6 @@ class MediaFragment : Fragment() {
binding.viewsRecyclerView.isVisible = false binding.viewsRecyclerView.isVisible = false
binding.errorLayout.errorPanel.isVisible = true binding.errorLayout.errorPanel.isVisible = true
checkIfLoginRequired(uiState.error.message) checkIfLoginRequired(uiState.error.message)
} }
private fun navigateToLibraryFragment(library: BaseItemDto) { private fun navigateToLibraryFragment(library: BaseItemDto) {
@ -151,4 +161,4 @@ class MediaFragment : Fragment() {
MediaFragmentDirections.actionNavigationMediaToSearchResultFragment(query) MediaFragmentDirections.actionNavigationMediaToSearchResultFragment(query)
) )
} }
} }

View file

@ -31,11 +31,11 @@ import dev.jdtech.jellyfin.utils.setTintColor
import dev.jdtech.jellyfin.utils.setTintColorAttribute import dev.jdtech.jellyfin.utils.setTintColorAttribute
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import java.util.UUID
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber import timber.log.Timber
import java.util.UUID
@AndroidEntryPoint @AndroidEntryPoint
class MediaInfoFragment : Fragment() { class MediaInfoFragment : Fragment() {
@ -48,7 +48,8 @@ class MediaInfoFragment : Fragment() {
lateinit var errorDialog: ErrorDialogFragment lateinit var errorDialog: ErrorDialogFragment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentMediaInfoBinding.inflate(inflater, container, false) binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
@ -111,9 +112,12 @@ class MediaInfoFragment : Fragment() {
} }
binding.seasonsRecyclerView.adapter = binding.seasonsRecyclerView.adapter =
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { season -> ViewItemListAdapter(
navigateToSeasonFragment(season) ViewItemListAdapter.OnClickListener { season ->
}, fixedWidth = true) navigateToSeasonFragment(season)
},
fixedWidth = true
)
binding.peopleRecyclerView.adapter = PersonListAdapter { person -> binding.peopleRecyclerView.adapter = PersonListAdapter { person ->
navigateToPersonDetail(person.id) navigateToPersonDetail(person.id)
} }
@ -121,7 +125,7 @@ class MediaInfoFragment : Fragment() {
binding.playButton.setOnClickListener { binding.playButton.setOnClickListener {
binding.playButton.setImageResource(android.R.color.transparent) binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.isVisible = true binding.progressCircular.isVisible = true
if (viewModel.canRetry){ if (viewModel.canRetry) {
binding.playButton.isEnabled = false binding.playButton.isEnabled = false
viewModel.download() viewModel.download()
return@setOnClickListener return@setOnClickListener
@ -187,7 +191,6 @@ class MediaInfoFragment : Fragment() {
) )
) )
} }
} else { } else {
binding.favoriteButton.isVisible = false binding.favoriteButton.isVisible = false
binding.checkButton.isVisible = false binding.checkButton.isVisible = false
@ -225,7 +228,6 @@ class MediaInfoFragment : Fragment() {
false -> binding.checkButton.setTintColorAttribute(R.attr.colorOnSecondaryContainer, requireActivity().theme) false -> binding.checkButton.setTintColorAttribute(R.attr.colorOnSecondaryContainer, requireActivity().theme)
} }
// Favorite icon // Favorite icon
val favoriteDrawable = when (favorite) { val favoriteDrawable = when (favorite) {
true -> R.drawable.ic_heart_filled true -> R.drawable.ic_heart_filled
@ -248,7 +250,6 @@ class MediaInfoFragment : Fragment() {
} }
} }
binding.name.text = item.name binding.name.text = item.name
binding.originalTitle.text = item.originalTitle binding.originalTitle.text = item.originalTitle
if (dateString.isEmpty()) { if (dateString.isEmpty()) {
@ -363,4 +364,4 @@ class MediaInfoFragment : Fragment() {
MediaInfoFragmentDirections.actionMediaInfoFragmentToPersonDetailFragment(personId) MediaInfoFragmentDirections.actionMediaInfoFragmentToPersonDetailFragment(personId)
) )
} }
} }

View file

@ -146,4 +146,4 @@ internal class PersonDetailFragment : Fragment() {
) )
) )
} }
} }

View file

@ -1,11 +1,11 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -34,7 +34,8 @@ class SearchResultFragment : Fragment() {
private lateinit var errorDialog: ErrorDialogFragment private lateinit var errorDialog: ErrorDialogFragment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentSearchResultBinding.inflate(inflater, container, false) binding = FragmentSearchResultBinding.inflate(inflater, container, false)
@ -42,9 +43,11 @@ class SearchResultFragment : Fragment() {
binding.searchResultsRecyclerView.adapter = FavoritesListAdapter( binding.searchResultsRecyclerView.adapter = FavoritesListAdapter(
ViewItemListAdapter.OnClickListener { item -> ViewItemListAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item) navigateToMediaInfoFragment(item)
}, HomeEpisodeListAdapter.OnClickListener { item -> },
HomeEpisodeListAdapter.OnClickListener { item ->
navigateToEpisodeBottomSheetFragment(item) navigateToEpisodeBottomSheetFragment(item)
}) }
)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -73,7 +76,6 @@ class SearchResultFragment : Fragment() {
errorDialog.show(parentFragmentManager, "errordialog") errorDialog.show(parentFragmentManager, "errordialog")
} }
return binding.root return binding.root
} }
@ -119,4 +121,4 @@ class SearchResultFragment : Fragment() {
) )
) )
} }
} }

View file

@ -1,11 +1,11 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -32,7 +32,8 @@ class SeasonFragment : Fragment() {
private lateinit var errorDialog: ErrorDialogFragment private lateinit var errorDialog: ErrorDialogFragment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentSeasonBinding.inflate(inflater, container, false) binding = FragmentSeasonBinding.inflate(inflater, container, false)
@ -70,10 +71,12 @@ class SeasonFragment : Fragment() {
} }
binding.episodesRecyclerView.adapter = binding.episodesRecyclerView.adapter =
EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode -> EpisodeListAdapter(
navigateToEpisodeBottomSheetFragment(episode) EpisodeListAdapter.OnClickListener { episode ->
}, args.seriesId, args.seriesName, args.seasonId, args.seasonName) navigateToEpisodeBottomSheetFragment(episode)
},
args.seriesId, args.seriesName, args.seasonId, args.seasonName
)
} }
private fun bindUiStateNormal(uiState: SeasonViewModel.UiState.Normal) { private fun bindUiStateNormal(uiState: SeasonViewModel.UiState.Normal) {
@ -106,4 +109,4 @@ class SeasonFragment : Fragment() {
) )
) )
} }
} }

View file

@ -1,19 +1,19 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
import dev.jdtech.jellyfin.databinding.FragmentServerSelectBinding import dev.jdtech.jellyfin.databinding.FragmentServerSelectBinding
import dev.jdtech.jellyfin.dialogs.DeleteServerDialogFragment import dev.jdtech.jellyfin.dialogs.DeleteServerDialogFragment
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -24,7 +24,8 @@ class ServerSelectFragment : Fragment() {
private val viewModel: ServerSelectViewModel by viewModels() private val viewModel: ServerSelectViewModel by viewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentServerSelectBinding.inflate(inflater) binding = FragmentServerSelectBinding.inflate(inflater)
@ -34,15 +35,18 @@ class ServerSelectFragment : Fragment() {
binding.viewModel = viewModel binding.viewModel = viewModel
binding.serversRecyclerView.adapter = binding.serversRecyclerView.adapter =
ServerGridAdapter(ServerGridAdapter.OnClickListener { server -> ServerGridAdapter(
viewModel.connectToServer(server) ServerGridAdapter.OnClickListener { server ->
}, ServerGridAdapter.OnLongClickListener { server -> viewModel.connectToServer(server)
DeleteServerDialogFragment(viewModel, server).show( },
parentFragmentManager, ServerGridAdapter.OnLongClickListener { server ->
"deleteServer" DeleteServerDialogFragment(viewModel, server).show(
) parentFragmentManager,
true "deleteServer"
}) )
true
}
)
binding.buttonAddServer.setOnClickListener { binding.buttonAddServer.setOnClickListener {
navigateToAddServerFragment() navigateToAddServerFragment()
@ -70,4 +74,4 @@ class ServerSelectFragment : Fragment() {
private fun navigateToMainActivity() { private fun navigateToMainActivity() {
findNavController().navigate(ServerSelectFragmentDirections.actionServerSelectFragmentToHomeFragment()) findNavController().navigate(ServerSelectFragmentDirections.actionServerSelectFragmentToHomeFragment())
} }
} }

View file

@ -20,4 +20,4 @@ class SettingsAppearanceFragment : PreferenceFragmentCompat() {
true true
} }
} }
} }

View file

@ -15,4 +15,4 @@ class SettingsCacheFragment : PreferenceFragmentCompat() {
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
} }
} }
} }

View file

@ -22,4 +22,4 @@ class SettingsDeviceFragment : PreferenceFragmentCompat() {
true true
} }
} }
} }

View file

@ -9,4 +9,4 @@ class SettingsDownloadsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.fragment_settings_downloads, rootKey) setPreferencesFromResource(R.xml.fragment_settings_downloads, rootKey)
} }
} }

View file

@ -8,7 +8,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
class SettingsFragment: PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.fragment_settings, rootKey) setPreferencesFromResource(R.xml.fragment_settings, rootKey)
@ -31,4 +31,4 @@ class SettingsFragment: PreferenceFragmentCompat() {
true true
} }
} }
} }

View file

@ -9,4 +9,4 @@ class SettingsLanguageFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.fragment_settings_language, rootKey) setPreferencesFromResource(R.xml.fragment_settings_language, rootKey)
} }
} }

View file

@ -24,4 +24,4 @@ class SettingsPlayerFragment : PreferenceFragmentCompat() {
true true
} }
} }
} }

View file

@ -7,4 +7,4 @@ class TwoPaneSettingsFragment : PreferenceHeaderFragmentCompat() {
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat { override fun onCreatePreferenceHeader(): PreferenceFragmentCompat {
return SettingsFragment() return SettingsFragment()
} }
} }

View file

@ -13,4 +13,4 @@ enum class CollectionType(val type: String) {
HomeVideos, Music, Playlists, Books, LiveTv, BoxSets HomeVideos, Music, Playlists, Books, LiveTv, BoxSets
) )
} }
} }

View file

@ -4,4 +4,4 @@ data class DiscoveredServer(
val id: String, val id: String,
val name: String, val name: String,
val address: String val address: String
) )

View file

@ -3,9 +3,9 @@ package dev.jdtech.jellyfin.models
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.util.UUID
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import java.util.*
@Parcelize @Parcelize
@Entity(tableName = "downloads") @Entity(tableName = "downloads")
@ -23,4 +23,4 @@ data class DownloadItem(
val playbackPosition: Long? = null, val playbackPosition: Long? = null,
val playedPercentage: Double? = null, val playedPercentage: Double? = null,
val downloadId: Long? = null, val downloadId: Long? = null,
) : Parcelable ) : Parcelable

View file

@ -1,10 +1,10 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import java.util.* import java.util.UUID
data class DownloadSection( data class DownloadSection(
val id: UUID, val id: UUID,
val name: String, val name: String,
val items: List<PlayerItem>? = null, val items: List<PlayerItem>? = null,
val series: List<DownloadSeriesMetadata>? = null val series: List<DownloadSeriesMetadata>? = null
) )

View file

@ -1,12 +1,12 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import android.os.Parcelable import android.os.Parcelable
import java.util.UUID
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize @Parcelize
data class DownloadSeriesMetadata( data class DownloadSeriesMetadata(
val itemId: UUID, val itemId: UUID,
val name: String? = null, val name: String? = null,
val episodes: List<PlayerItem> val episodes: List<PlayerItem>
) : Parcelable ) : Parcelable

View file

@ -10,4 +10,4 @@ class ExternalSubtitle(
val language: String, val language: String,
val uri: Uri, val uri: Uri,
val mimeType: String, val mimeType: String,
) : Parcelable ) : Parcelable

View file

@ -6,4 +6,4 @@ data class FavoriteSection(
val id: Int, val id: Int,
val name: String, val name: String,
var items: List<BaseItemDto> var items: List<BaseItemDto>
) )

View file

@ -1,10 +1,10 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.*
data class HomeSection( data class HomeSection(
val id: UUID, val id: UUID,
val name: String, val name: String,
var items: List<BaseItemDto> var items: List<BaseItemDto>
) )

View file

@ -1,8 +1,8 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.UUID import java.util.UUID
import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class PlayerItem( data class PlayerItem(
@ -15,4 +15,4 @@ data class PlayerItem(
val indexNumber: Int? = null, val indexNumber: Int? = null,
val item: DownloadItem? = null, val item: DownloadItem? = null,
val externalSubtitles: List<ExternalSubtitle> = emptyList() val externalSubtitles: List<ExternalSubtitle> = emptyList()
) : Parcelable ) : Parcelable

View file

@ -5,4 +5,4 @@ import java.util.UUID
data class User( data class User(
val id: UUID, val id: UUID,
val name: String val name: String
) )

View file

@ -1,11 +1,11 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.UUID import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
data class View( data class View(
val id: UUID, val id: UUID,
val name: String?, val name: String?,
var items: List<BaseItemDto>? = null, var items: List<BaseItemDto>? = null,
val type: String? val type: String?
) )

View file

@ -1,6 +1,5 @@
package dev.jdtech.jellyfin.mpv package dev.jdtech.jellyfin.mpv
import `is`.xyz.mpv.MPVLib
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.AssetManager import android.content.res.AssetManager
@ -13,24 +12,39 @@ import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import android.view.TextureView import android.view.TextureView
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.BasePlayer
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DeviceInfo
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.Player.Commands import com.google.android.exoplayer2.Player.Commands
import com.google.android.exoplayer2.Timeline
import com.google.android.exoplayer2.Tracks
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.source.TrackGroup import com.google.android.exoplayer2.source.TrackGroup
import com.google.android.exoplayer2.text.Cue import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.text.CueGroup import com.google.android.exoplayer2.text.CueGroup
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
import com.google.android.exoplayer2.util.* import com.google.android.exoplayer2.util.Clock
import com.google.android.exoplayer2.util.FlagSet
import com.google.android.exoplayer2.util.ListenerSet
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.util.Util
import com.google.android.exoplayer2.video.VideoSize import com.google.android.exoplayer2.video.VideoSize
import dev.jdtech.jellyfin.utils.AppPreferences import dev.jdtech.jellyfin.utils.AppPreferences
import `is`.xyz.mpv.MPVLib
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.CopyOnWriteArraySet
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.CopyOnWriteArraySet
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
class MPVPlayer( class MPVPlayer(
@ -627,16 +641,16 @@ class MPVPlayer(
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem() && !isPlayingAd) .addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem() && !isPlayingAd)
.addIf( .addIf(
COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS,
!currentTimeline.isEmpty !currentTimeline.isEmpty &&
&& (hasPreviousMediaItem() || !isCurrentMediaItemLive || isCurrentMediaItemSeekable) (hasPreviousMediaItem() || !isCurrentMediaItemLive || isCurrentMediaItemSeekable) &&
&& !isPlayingAd !isPlayingAd
) )
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem() && !isPlayingAd) .addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem() && !isPlayingAd)
.addIf( .addIf(
COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT,
!currentTimeline.isEmpty !currentTimeline.isEmpty &&
&& (hasNextMediaItem() || (isCurrentMediaItemLive && isCurrentMediaItemDynamic)) (hasNextMediaItem() || (isCurrentMediaItemLive && isCurrentMediaItemDynamic)) &&
&& !isPlayingAd !isPlayingAd
) )
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd) .addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd)
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable && !isPlayingAd) .addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable && !isPlayingAd)
@ -655,7 +669,7 @@ class MPVPlayer(
tracks = Tracks.EMPTY tracks = Tracks.EMPTY
playbackParameters = PlaybackParameters.DEFAULT playbackParameters = PlaybackParameters.DEFAULT
initialCommands.clear() initialCommands.clear()
//initialSeekTo = 0L // initialSeekTo = 0L
} }
/** Prepares the player. */ /** Prepares the player. */
@ -1212,8 +1226,7 @@ class MPVPlayer(
MPVLib.setOptionString("panscan", "1") MPVLib.setOptionString("panscan", "1")
MPVLib.setOptionString("sub-use-margins", "yes") MPVLib.setOptionString("sub-use-margins", "yes")
MPVLib.setOptionString("sub-ass-force-margins", "yes") MPVLib.setOptionString("sub-ass-force-margins", "yes")
} } else {
else {
MPVLib.setOptionString("panscan", "0") MPVLib.setOptionString("panscan", "0")
MPVLib.setOptionString("sub-use-margins", "no") MPVLib.setOptionString("sub-use-margins", "no")
MPVLib.setOptionString("sub-ass-force-margins", "no") MPVLib.setOptionString("sub-ass-force-margins", "no")
@ -1409,7 +1422,8 @@ class MPVPlayer(
IntArray(this.length) { C.FORMAT_HANDLED }, IntArray(this.length) { C.FORMAT_HANDLED },
BooleanArray(this.length) { it == indexCurrentVideo } BooleanArray(this.length) { it == indexCurrentVideo }
) )
}) }
)
} }
if (trackListAudio.isNotEmpty()) { if (trackListAudio.isNotEmpty()) {
trackGroups.add( trackGroups.add(
@ -1420,7 +1434,8 @@ class MPVPlayer(
IntArray(this.length) { C.FORMAT_HANDLED }, IntArray(this.length) { C.FORMAT_HANDLED },
BooleanArray(this.length) { it == indexCurrentAudio } BooleanArray(this.length) { it == indexCurrentAudio }
) )
}) }
)
} }
if (trackListText.isNotEmpty()) { if (trackListText.isNotEmpty()) {
trackGroups.add( trackGroups.add(
@ -1431,7 +1446,8 @@ class MPVPlayer(
IntArray(this.length) { C.FORMAT_HANDLED }, IntArray(this.length) { C.FORMAT_HANDLED },
BooleanArray(this.length) { it == indexCurrentText } BooleanArray(this.length) { it == indexCurrentText }
) )
}) }
)
} }
if (trackGroups.isNotEmpty()) { if (trackGroups.isNotEmpty()) {
tracks = Tracks(trackGroups) tracks = Tracks(trackGroups)

View file

@ -4,11 +4,11 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import timber.log.Timber import timber.log.Timber
import java.util.*
class ItemsPagingSource( class ItemsPagingSource(
private val jellyfinApi: JellyfinApi, private val jellyfinApi: JellyfinApi,
@ -25,14 +25,14 @@ class ItemsPagingSource(
return try { return try {
val response = jellyfinApi.itemsApi.getItems( val response = jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
parentId = parentId, parentId = parentId,
includeItemTypes = includeTypes, includeItemTypes = includeTypes,
recursive = recursive, recursive = recursive,
sortBy = listOf(sortBy.SortString), sortBy = listOf(sortBy.SortString),
sortOrder = listOf(sortOrder), sortOrder = listOf(sortOrder),
startIndex = position, startIndex = position,
limit = params.loadSize limit = params.loadSize
).content.items.orEmpty() ).content.items.orEmpty()
LoadResult.Page( LoadResult.Page(
data = response, data = response,
@ -47,4 +47,4 @@ class ItemsPagingSource(
override fun getRefreshKey(state: PagingState<Int, BaseItemDto>): Int { override fun getRefreshKey(state: PagingState<Int, BaseItemDto>): Int {
return 0 return 0
} }
} }

View file

@ -1,11 +1,14 @@
package dev.jdtech.jellyfin.repository package dev.jdtech.jellyfin.repository
import androidx.paging.PagingData import androidx.paging.PagingData
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import java.util.UUID
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.model.api.* import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.* import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.SortOrder
interface JellyfinRepository { interface JellyfinRepository {
suspend fun getUserViews(): List<BaseItemDto> suspend fun getUserViews(): List<BaseItemDto>
@ -78,4 +81,4 @@ interface JellyfinRepository {
fun getBaseUrl(): String fun getBaseUrl(): String
suspend fun updateDeviceName(name: String) suspend fun updateDeviceName(name: String)
} }

View file

@ -5,12 +5,25 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import java.util.UUID
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.* import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.DeviceOptionsDto
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.DirectPlayProfile
import org.jellyfin.sdk.model.api.DlnaProfileType
import org.jellyfin.sdk.model.api.GeneralCommandType
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemFilter
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.PlaybackInfoDto
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile
import timber.log.Timber import timber.log.Timber
import java.util.*
class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRepository { class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRepository {
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) { override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) {
@ -150,7 +163,8 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
override suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo> = override suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
itemId, PlaybackInfoDto( itemId,
PlaybackInfoDto(
userId = jellyfinApi.userId!!, userId = jellyfinApi.userId!!,
deviceProfile = DeviceProfile( deviceProfile = DeviceProfile(
name = "Direct play all", name = "Direct play all",
@ -220,7 +234,8 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
GeneralCommandType.PLAY_STATE, GeneralCommandType.PLAY_STATE,
GeneralCommandType.PLAY_NEXT, GeneralCommandType.PLAY_NEXT,
GeneralCommandType.PLAY_MEDIA_SOURCE GeneralCommandType.PLAY_MEDIA_SOURCE
), supportsMediaControl = true ),
supportsMediaControl = true
) )
} }
} }

View file

@ -70,13 +70,15 @@ internal class TvPlayerActivity : BasePlayerActivity() {
when { when {
viewModel.player.isPlaying -> { viewModel.player.isPlaying -> {
viewModel.player.pause() viewModel.player.pause()
setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_play, theme) setImageDrawable(
ResourcesCompat.getDrawable(resources, R.drawable.ic_play, theme)
) )
} }
viewModel.player.isLoading -> Unit viewModel.player.isLoading -> Unit
else -> { else -> {
viewModel.player.play() viewModel.player.play()
setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_play, theme) setImageDrawable(
ResourcesCompat.getDrawable(resources, R.drawable.ic_play, theme)
) )
} }
} }
@ -155,4 +157,4 @@ internal class TvPlayerActivity : BasePlayerActivity() {
return popup return popup
} }
} }

View file

@ -10,7 +10,11 @@ import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.* import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.DiffCallback
import androidx.leanback.widget.HeaderItem
import androidx.leanback.widget.ListRow
import androidx.leanback.widget.ListRowPresenter
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@ -146,15 +150,21 @@ internal class HomeFragment : BrowseSupportFragment() {
adapterMap[name]?.setItems(items, diffCallback) adapterMap[name]?.setItems(items, diffCallback)
} else { } else {
adapterMap[name] = when (this) { adapterMap[name] = when (this) {
is HomeItem.Libraries -> ArrayObjectAdapter(LibaryItemPresenter { item -> is HomeItem.Libraries -> ArrayObjectAdapter(
navigateToLibraryFragment(item) LibaryItemPresenter { item ->
}).apply { setItems(items, diffCallback) } navigateToLibraryFragment(item)
is HomeItem.Section -> ArrayObjectAdapter(DynamicMediaItemPresenter { item -> }
navigateToMediaDetailFragment(item) ).apply { setItems(items, diffCallback) }
}).apply { setItems(items, diffCallback) } is HomeItem.Section -> ArrayObjectAdapter(
is HomeItem.ViewItem -> ArrayObjectAdapter(MediaItemPresenter { item -> DynamicMediaItemPresenter { item ->
navigateToMediaDetailFragment(item) navigateToMediaDetailFragment(item)
}).apply { setItems(items, diffCallback) } }
).apply { setItems(items, diffCallback) }
is HomeItem.ViewItem -> ArrayObjectAdapter(
MediaItemPresenter { item ->
navigateToMediaDetailFragment(item)
}
).apply { setItems(items, diffCallback) }
} }
} }

View file

@ -76,7 +76,8 @@ internal class MediaDetailFragment : Fragment() {
val seasonsAdapter = ViewItemListAdapter( val seasonsAdapter = ViewItemListAdapter(
fixedWidth = true, fixedWidth = true,
onClickListener = ViewItemListAdapter.OnClickListener {}) onClickListener = ViewItemListAdapter.OnClickListener {}
)
binding.seasonsRow.gridView.adapter = seasonsAdapter binding.seasonsRow.gridView.adapter = seasonsAdapter
binding.seasonsRow.gridView.verticalSpacing = 25 binding.seasonsRow.gridView.verticalSpacing = 25
@ -254,4 +255,4 @@ internal class MediaDetailFragment : Fragment() {
) )
) )
} }
} }

View file

@ -105,4 +105,4 @@ class DynamicMediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Pr
} }
override fun onUnbindViewHolder(viewHolder: ViewHolder) = Unit override fun onUnbindViewHolder(viewHolder: ViewHolder) = Unit
} }

View file

@ -68,4 +68,4 @@ class TrackSelectorAdapter(
val selected: Boolean, val selected: Boolean,
val playerTrack: MPVPlayer.Companion.Track val playerTrack: MPVPlayer.Companion.Track
) )
} }

View file

@ -27,4 +27,4 @@ object Constants {
const val FAVORITE_TYPE_MOVIES = 0 const val FAVORITE_TYPE_MOVIES = 0
const val FAVORITE_TYPE_SHOWS = 1 const val FAVORITE_TYPE_SHOWS = 1
const val FAVORITE_TYPE_EPISODES = 2 const val FAVORITE_TYPE_EPISODES = 2
} }

View file

@ -11,12 +11,12 @@ import dev.jdtech.jellyfin.models.DownloadItem
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import java.io.File
import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.UserItemDataDto import org.jellyfin.sdk.model.api.UserItemDataDto
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.util.UUID
var defaultStorage: File? = null var defaultStorage: File? = null
@ -76,7 +76,7 @@ fun loadDownloadLocation(context: Context) {
fun checkDownloadStatus(downloadDatabase: DownloadDatabaseDao, context: Context) { fun checkDownloadStatus(downloadDatabase: DownloadDatabaseDao, context: Context) {
val items = downloadDatabase.loadItems() val items = downloadDatabase.loadItems()
for (item in items) { for (item in items) {
try{ try {
val query = DownloadManager.Query() val query = DownloadManager.Query()
.setFilterById(item.downloadId!!) .setFilterById(item.downloadId!!)
val result = context.getSystemService<DownloadManager>()!!.query(query) val result = context.getSystemService<DownloadManager>()!!.query(query)
@ -156,7 +156,6 @@ fun deleteDownloadedEpisode(downloadDatabase: DownloadDatabaseDao, itemId: UUID)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
} }
fun postDownloadPlaybackProgress( fun postDownloadPlaybackProgress(
@ -277,6 +276,5 @@ suspend fun syncPlaybackProgress(
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
} }
} }

View file

@ -14,15 +14,15 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import dev.jdtech.jellyfin.PlayerActivity import dev.jdtech.jellyfin.PlayerActivity
import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.mpv.MPVPlayer
import timber.log.Timber
import kotlin.math.abs import kotlin.math.abs
import timber.log.Timber
class PlayerGestureHelper( class PlayerGestureHelper(
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
private val activity: PlayerActivity, private val activity: PlayerActivity,
private val playerView: StyledPlayerView, private val playerView: StyledPlayerView,
private val audioManager: AudioManager private val audioManager: AudioManager
) { ) {
/** /**
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides. * Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
* Useful on wide-screen phones to remove black bars from some movies. * Useful on wide-screen phones to remove black bars from some movies.
@ -45,133 +45,143 @@ class PlayerGestureHelper(
private var lastScaleEvent: Long = 0 private var lastScaleEvent: Long = 0
private val tapGestureDetector = GestureDetector(playerView.context, object : GestureDetector.SimpleOnGestureListener() { private val tapGestureDetector = GestureDetector(
override fun onSingleTapConfirmed(e: MotionEvent): Boolean { playerView.context,
playerView.apply { object : GestureDetector.SimpleOnGestureListener() {
if (!isControllerFullyVisible) showController() else hideController() override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
} playerView.apply {
if (!isControllerFullyVisible) showController() else hideController()
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
val viewCenterX = playerView.measuredWidth / 2
val currentPos = playerView.player?.currentPosition ?: 0
if (e.x.toInt() > viewCenterX) {
playerView.player?.seekTo(currentPos + appPreferences.playerSeekForwardIncrement)
}
else {
playerView.player?.seekTo((currentPos - appPreferences.playerSeekBackIncrement).coerceAtLeast(0))
}
return true
}
})
private val gestureDetector = GestureDetector(playerView.context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
playerView.apply {
if (!isControllerFullyVisible) showController() else hideController()
}
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
val viewCenterX = playerView.measuredWidth / 2
val currentPos = playerView.player?.currentPosition ?: 0
if (e.x.toInt() > viewCenterX) {
playerView.player?.seekTo(currentPos + appPreferences.playerSeekForwardIncrement)
}
else {
playerView.player?.seekTo((currentPos - appPreferences.playerSeekBackIncrement).coerceAtLeast(0))
}
return true
}
@SuppressLint("SetTextI18n")
override fun onScroll(firstEvent: MotionEvent, currentEvent: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if (firstEvent.y < playerView.resources.dip(Constants.GESTURE_EXCLUSION_AREA_TOP))
return false
// Check whether swipe was oriented vertically
if (abs(distanceY / distanceX) < 2) {
return if ((abs(currentEvent.x - firstEvent.x) > 50 || swipeGestureProgressOpen) &&
!swipeGestureBrightnessOpen &&
!swipeGestureVolumeOpen &&
(SystemClock.elapsedRealtime() - lastScaleEvent) > 200) {
val currentPos = playerView.player?.currentPosition ?: 0
val vidDuration = (playerView.player?.duration ?: 0).coerceAtLeast(0)
val difference = ((currentEvent.x - firstEvent.x) * 90).toLong()
val newPos = (currentPos + difference).coerceIn(0, vidDuration)
activity.binding.progressScrubberLayout.visibility = View.VISIBLE
activity.binding.progressScrubberText.text = "${longToTimestamp(difference)} [${longToTimestamp(newPos, true)}]"
swipeGestureValueTrackerProgress = newPos
swipeGestureProgressOpen = true
true
} else false
}
if (swipeGestureValueTrackerProgress > -1 || swipeGestureProgressOpen)
return false
val viewCenterX = playerView.measuredWidth / 2
// Distance to swipe to go from min to max
val distanceFull = playerView.measuredHeight * Constants.FULL_SWIPE_RANGE_SCREEN_RATIO
val ratioChange = distanceY / distanceFull
if (firstEvent.x.toInt() > viewCenterX) {
// Swiping on the right, change volume
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
if (swipeGestureValueTrackerVolume == -1f) swipeGestureValueTrackerVolume = currentVolume.toFloat()
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val change = ratioChange * maxVolume
swipeGestureValueTrackerVolume += change
val toSet = swipeGestureValueTrackerVolume.toInt().coerceIn(0, maxVolume)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, toSet, 0)
activity.binding.gestureVolumeLayout.visibility = View.VISIBLE
activity.binding.gestureVolumeProgressBar.max = maxVolume
activity.binding.gestureVolumeProgressBar.progress = toSet
activity.binding.gestureVolumeText.text = "${(toSet.toFloat()/maxVolume.toFloat()).times(100).toInt()}%"
swipeGestureVolumeOpen = true
} else {
// Swiping on the left, change brightness
val window = activity.window
val brightnessRange = BRIGHTNESS_OVERRIDE_OFF..BRIGHTNESS_OVERRIDE_FULL
// Initialize on first swipe
if (swipeGestureValueTrackerBrightness == -1f) {
val brightness = window.attributes.screenBrightness
Timber.d("Brightness ${Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS)}")
swipeGestureValueTrackerBrightness = when (brightness) {
in brightnessRange -> brightness
else -> Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS)/255
}
} }
swipeGestureValueTrackerBrightness = (swipeGestureValueTrackerBrightness + ratioChange).coerceIn(brightnessRange)
val lp = window.attributes
lp.screenBrightness = swipeGestureValueTrackerBrightness
window.attributes = lp
activity.binding.gestureBrightnessLayout.visibility = View.VISIBLE return true
activity.binding.gestureBrightnessProgressBar.max = BRIGHTNESS_OVERRIDE_FULL.times(100).toInt() }
activity.binding.gestureBrightnessProgressBar.progress = lp.screenBrightness.times(100).toInt()
activity.binding.gestureBrightnessText.text = "${(lp.screenBrightness/BRIGHTNESS_OVERRIDE_FULL).times(100).toInt()}%" override fun onDoubleTap(e: MotionEvent): Boolean {
val viewCenterX = playerView.measuredWidth / 2
swipeGestureBrightnessOpen = true val currentPos = playerView.player?.currentPosition ?: 0
if (e.x.toInt() > viewCenterX) {
playerView.player?.seekTo(currentPos + appPreferences.playerSeekForwardIncrement)
} else {
playerView.player?.seekTo((currentPos - appPreferences.playerSeekBackIncrement).coerceAtLeast(0))
}
return true
} }
return true
} }
}) )
private val gestureDetector = GestureDetector(
playerView.context,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
playerView.apply {
if (!isControllerFullyVisible) showController() else hideController()
}
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
val viewCenterX = playerView.measuredWidth / 2
val currentPos = playerView.player?.currentPosition ?: 0
if (e.x.toInt() > viewCenterX) {
playerView.player?.seekTo(currentPos + appPreferences.playerSeekForwardIncrement)
} else {
playerView.player?.seekTo((currentPos - appPreferences.playerSeekBackIncrement).coerceAtLeast(0))
}
return true
}
@SuppressLint("SetTextI18n")
override fun onScroll(
firstEvent: MotionEvent,
currentEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (firstEvent.y < playerView.resources.dip(Constants.GESTURE_EXCLUSION_AREA_TOP))
return false
// Check whether swipe was oriented vertically
if (abs(distanceY / distanceX) < 2) {
return if ((abs(currentEvent.x - firstEvent.x) > 50 || swipeGestureProgressOpen) &&
!swipeGestureBrightnessOpen &&
!swipeGestureVolumeOpen &&
(SystemClock.elapsedRealtime() - lastScaleEvent) > 200
) {
val currentPos = playerView.player?.currentPosition ?: 0
val vidDuration = (playerView.player?.duration ?: 0).coerceAtLeast(0)
val difference = ((currentEvent.x - firstEvent.x) * 90).toLong()
val newPos = (currentPos + difference).coerceIn(0, vidDuration)
activity.binding.progressScrubberLayout.visibility = View.VISIBLE
activity.binding.progressScrubberText.text = "${longToTimestamp(difference)} [${longToTimestamp(newPos, true)}]"
swipeGestureValueTrackerProgress = newPos
swipeGestureProgressOpen = true
true
} else false
}
if (swipeGestureValueTrackerProgress > -1 || swipeGestureProgressOpen)
return false
val viewCenterX = playerView.measuredWidth / 2
// Distance to swipe to go from min to max
val distanceFull = playerView.measuredHeight * Constants.FULL_SWIPE_RANGE_SCREEN_RATIO
val ratioChange = distanceY / distanceFull
if (firstEvent.x.toInt() > viewCenterX) {
// Swiping on the right, change volume
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
if (swipeGestureValueTrackerVolume == -1f) swipeGestureValueTrackerVolume = currentVolume.toFloat()
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val change = ratioChange * maxVolume
swipeGestureValueTrackerVolume += change
val toSet = swipeGestureValueTrackerVolume.toInt().coerceIn(0, maxVolume)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, toSet, 0)
activity.binding.gestureVolumeLayout.visibility = View.VISIBLE
activity.binding.gestureVolumeProgressBar.max = maxVolume
activity.binding.gestureVolumeProgressBar.progress = toSet
activity.binding.gestureVolumeText.text = "${(toSet.toFloat() / maxVolume.toFloat()).times(100).toInt()}%"
swipeGestureVolumeOpen = true
} else {
// Swiping on the left, change brightness
val window = activity.window
val brightnessRange = BRIGHTNESS_OVERRIDE_OFF..BRIGHTNESS_OVERRIDE_FULL
// Initialize on first swipe
if (swipeGestureValueTrackerBrightness == -1f) {
val brightness = window.attributes.screenBrightness
Timber.d("Brightness ${Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS)}")
swipeGestureValueTrackerBrightness = when (brightness) {
in brightnessRange -> brightness
else -> Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS) / 255
}
}
swipeGestureValueTrackerBrightness = (swipeGestureValueTrackerBrightness + ratioChange).coerceIn(brightnessRange)
val lp = window.attributes
lp.screenBrightness = swipeGestureValueTrackerBrightness
window.attributes = lp
activity.binding.gestureBrightnessLayout.visibility = View.VISIBLE
activity.binding.gestureBrightnessProgressBar.max = BRIGHTNESS_OVERRIDE_FULL.times(100).toInt()
activity.binding.gestureBrightnessProgressBar.progress = lp.screenBrightness.times(100).toInt()
activity.binding.gestureBrightnessText.text = "${(lp.screenBrightness / BRIGHTNESS_OVERRIDE_FULL).times(100).toInt()}%"
swipeGestureBrightnessOpen = true
}
return true
}
}
)
private val hideGestureVolumeIndicatorOverlayAction = Runnable { private val hideGestureVolumeIndicatorOverlayAction = Runnable {
activity.binding.gestureVolumeLayout.visibility = View.GONE activity.binding.gestureVolumeLayout.visibility = View.GONE
@ -191,27 +201,29 @@ class PlayerGestureHelper(
/** /**
* Handles scale/zoom gesture * Handles scale/zoom gesture
*/ */
private val zoomGestureDetector = ScaleGestureDetector(playerView.context, object : ScaleGestureDetector.OnScaleGestureListener { private val zoomGestureDetector = ScaleGestureDetector(
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true playerView.context,
object : ScaleGestureDetector.OnScaleGestureListener {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
override fun onScale(detector: ScaleGestureDetector): Boolean { override fun onScale(detector: ScaleGestureDetector): Boolean {
lastScaleEvent = SystemClock.elapsedRealtime() lastScaleEvent = SystemClock.elapsedRealtime()
val scaleFactor = detector.scaleFactor val scaleFactor = detector.scaleFactor
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) { if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
isZoomEnabled = scaleFactor > 1 isZoomEnabled = scaleFactor > 1
updateZoomMode(isZoomEnabled) updateZoomMode(isZoomEnabled)
}
return true
} }
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) = Unit override fun onScaleEnd(detector: ScaleGestureDetector) = Unit
}).apply { isQuickScaleEnabled = false } }
).apply { isQuickScaleEnabled = false }
private fun updateZoomMode(enabled: Boolean) { private fun updateZoomMode(enabled: Boolean) {
if (playerView.player is MPVPlayer) { if (playerView.player is MPVPlayer) {
(playerView.player as MPVPlayer).updateZoomMode(enabled) (playerView.player as MPVPlayer).updateZoomMode(enabled)
} } else {
else {
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
} }
} }

View file

@ -19,4 +19,4 @@ enum class SortBy(val SortString: String) {
} }
} }
} }
} }

View file

@ -23,7 +23,7 @@ fun BaseItemDto.toView(): View {
fun Fragment.checkIfLoginRequired(error: String?) { fun Fragment.checkIfLoginRequired(error: String?) {
if (error != null) { if (error != null) {
if (error.contains("401")) { if (error.contains("401")) {
Timber.d("Login required!") Timber.d("Login required!")
findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment()) findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment())
} }
@ -50,4 +50,4 @@ fun ImageButton.setTintColorAttribute(@AttrRes attributeId: Int, theme: Resource
theme theme
) )
) )
} }

View file

@ -11,13 +11,21 @@ import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.DiscoveredServer import dev.jdtech.jellyfin.models.DiscoveredServer
import kotlinx.coroutines.* import javax.inject.Inject
import kotlinx.coroutines.flow.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.discovery.RecommendedServerInfo import org.jellyfin.sdk.discovery.RecommendedServerInfo
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.discovery.RecommendedServerIssue import org.jellyfin.sdk.discovery.RecommendedServerIssue
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AddServerViewModel class AddServerViewModel
@ -54,11 +62,13 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val servers = jellyfinApi.jellyfin.discovery.discoverLocalServers() val servers = jellyfinApi.jellyfin.discovery.discoverLocalServers()
servers.collect { serverDiscoveryInfo -> servers.collect { serverDiscoveryInfo ->
discoveredServers.add(DiscoveredServer( discoveredServers.add(
serverDiscoveryInfo.id, DiscoveredServer(
serverDiscoveryInfo.name, serverDiscoveryInfo.id,
serverDiscoveryInfo.address serverDiscoveryInfo.name,
)) serverDiscoveryInfo.address
)
)
_discoveredServersState.emit( _discoveredServersState.emit(
DiscoveredServersState.Servers(ArrayList(discoveredServers)) DiscoveredServersState.Servers(ArrayList(discoveredServers))
) )
@ -131,7 +141,6 @@ constructor(
} }
} }
} catch (_: CancellationException) { } catch (_: CancellationException) {
} catch (e: Exception) { } catch (e: Exception) {
_uiState.emit( _uiState.emit(
UiState.Error( UiState.Error(
@ -215,4 +224,4 @@ constructor(
if (server != null) return true if (server != null) return true
return false return false
} }
} }

View file

@ -5,10 +5,10 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.adapters.DownloadEpisodeItem import dev.jdtech.jellyfin.adapters.DownloadEpisodeItem
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DownloadSeriesViewModel class DownloadSeriesViewModel
@ -36,8 +36,11 @@ constructor() : ViewModel() {
private fun getEpisodes(seriesMetadata: DownloadSeriesMetadata): List<DownloadEpisodeItem> { private fun getEpisodes(seriesMetadata: DownloadSeriesMetadata): List<DownloadEpisodeItem> {
val episodes = seriesMetadata.episodes val episodes = seriesMetadata.episodes
return listOf(DownloadEpisodeItem.Header) + episodes.sortedWith(compareBy( return listOf(DownloadEpisodeItem.Header) + episodes.sortedWith(
{ it.item!!.parentIndexNumber }, compareBy(
{ it.item!!.indexNumber })).map { DownloadEpisodeItem.Episode(it) } { it.item!!.parentIndexNumber },
{ it.item!!.indexNumber }
)
).map { DownloadEpisodeItem.Episode(it) }
} }
} }

View file

@ -1,7 +1,8 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import android.app.Application import android.app.Application
import androidx.lifecycle.* import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.DownloadSection import dev.jdtech.jellyfin.models.DownloadSection
@ -9,13 +10,14 @@ import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.checkDownloadStatus import dev.jdtech.jellyfin.utils.checkDownloadStatus
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
import kotlinx.coroutines.* import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import java.util.*
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DownloadViewModel class DownloadViewModel
@ -78,4 +80,4 @@ constructor(
} }
} }
} }
} }

View file

@ -1,12 +1,23 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import android.app.Application import android.app.Application
import androidx.lifecycle.* import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.* import dev.jdtech.jellyfin.utils.canRetryDownload
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import dev.jdtech.jellyfin.utils.isItemAvailable
import dev.jdtech.jellyfin.utils.isItemDownloaded
import dev.jdtech.jellyfin.utils.requestDownload
import java.text.DateFormat
import java.time.ZoneOffset
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -14,11 +25,6 @@ import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.model.DateTime import org.jellyfin.sdk.model.DateTime
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat
import java.time.ZoneOffset
import java.util.Date
import java.util.UUID
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class EpisodeBottomSheetViewModel class EpisodeBottomSheetViewModel
@ -178,4 +184,4 @@ constructor(
val date = Date.from(instant) val date = Date.from(instant)
return DateFormat.getDateInstance(DateFormat.SHORT).format(date) return DateFormat.getDateInstance(DateFormat.SHORT).format(date)
} }
} }

View file

@ -9,13 +9,13 @@ import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.models.FavoriteSection import dev.jdtech.jellyfin.models.FavoriteSection
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.Constants import dev.jdtech.jellyfin.utils.Constants
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FavoriteViewModel class FavoriteViewModel
@ -56,7 +56,8 @@ constructor(
FavoriteSection( FavoriteSection(
Constants.FAVORITE_TYPE_MOVIES, Constants.FAVORITE_TYPE_MOVIES,
resources.getString(R.string.movies_label), resources.getString(R.string.movies_label),
items.filter { it.type == BaseItemKind.MOVIE }).let { items.filter { it.type == BaseItemKind.MOVIE }
).let {
if (it.items.isNotEmpty()) favoriteSections.add( if (it.items.isNotEmpty()) favoriteSections.add(
it it
) )
@ -64,7 +65,8 @@ constructor(
FavoriteSection( FavoriteSection(
Constants.FAVORITE_TYPE_SHOWS, Constants.FAVORITE_TYPE_SHOWS,
resources.getString(R.string.shows_label), resources.getString(R.string.shows_label),
items.filter { it.type == BaseItemKind.SERIES }).let { items.filter { it.type == BaseItemKind.SERIES }
).let {
if (it.items.isNotEmpty()) favoriteSections.add( if (it.items.isNotEmpty()) favoriteSections.add(
it it
) )
@ -72,7 +74,8 @@ constructor(
FavoriteSection( FavoriteSection(
Constants.FAVORITE_TYPE_EPISODES, Constants.FAVORITE_TYPE_EPISODES,
resources.getString(R.string.episodes_label), resources.getString(R.string.episodes_label),
items.filter { it.type == BaseItemKind.EPISODE }).let { items.filter { it.type == BaseItemKind.EPISODE }
).let {
if (it.items.isNotEmpty()) favoriteSections.add( if (it.items.isNotEmpty()) favoriteSections.add(
it it
) )
@ -85,4 +88,4 @@ constructor(
} }
} }
} }
} }

View file

@ -14,13 +14,13 @@ import dev.jdtech.jellyfin.models.HomeSection
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.syncPlaybackProgress import dev.jdtech.jellyfin.utils.syncPlaybackProgress
import dev.jdtech.jellyfin.utils.toView import dev.jdtech.jellyfin.utils.toView
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.*
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject internal constructor( class HomeViewModel @Inject internal constructor(
@ -117,5 +117,3 @@ class HomeViewModel @Inject internal constructor(
.map { (view, latest) -> view.toView().apply { items = latest } } .map { (view, latest) -> view.toView().apply { items = latest } }
.map { ViewItem(it) } .map { ViewItem(it) }
} }

View file

@ -1,10 +1,13 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.* import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -13,8 +16,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LibraryViewModel class LibraryViewModel
@ -60,4 +61,4 @@ constructor(
} }
} }
} }
} }

View file

@ -2,7 +2,8 @@ package dev.jdtech.jellyfin.viewmodels
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import androidx.lifecycle.* import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.BaseApplication import dev.jdtech.jellyfin.BaseApplication
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
@ -10,6 +11,8 @@ import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.User import dev.jdtech.jellyfin.models.User
import javax.inject.Inject
import kotlin.Exception
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -18,8 +21,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.AuthenticateUserByName import org.jellyfin.sdk.model.api.AuthenticateUserByName
import javax.inject.Inject
import kotlin.Exception
@HiltViewModel @HiltViewModel
class LoginViewModel class LoginViewModel
@ -129,4 +130,4 @@ constructor(
database.insert(server) database.insert(server)
} }
} }
} }

View file

@ -5,4 +5,4 @@ import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
var startDestinationChanged = false var startDestinationChanged = false
var startDestinationTvChanged = false var startDestinationTvChanged = false
} }

View file

@ -7,7 +7,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.* import dev.jdtech.jellyfin.utils.canRetryDownload
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import dev.jdtech.jellyfin.utils.isItemAvailable
import dev.jdtech.jellyfin.utils.isItemDownloaded
import dev.jdtech.jellyfin.utils.requestDownload
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -18,8 +25,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import timber.log.Timber import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MediaInfoViewModel class MediaInfoViewModel
@ -267,4 +272,4 @@ constructor(
fun deleteItem() { fun deleteItem() {
deleteDownloadedEpisode(downloadDatabase, playerItem.itemId) deleteDownloadedEpisode(downloadDatabase, playerItem.itemId)
} }
} }

View file

@ -1,14 +1,15 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.* import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.CollectionType import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MediaViewModel class MediaViewModel
@ -45,4 +46,4 @@ constructor(
} }
} }
} }
} }

View file

@ -4,13 +4,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import java.util.UUID
import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class PersonDetailViewModel @Inject internal constructor( internal class PersonDetailViewModel @Inject internal constructor(
@ -66,4 +66,4 @@ internal class PersonDetailViewModel @Inject internal constructor(
val movies: List<BaseItemDto>, val movies: List<BaseItemDto>,
val shows: List<BaseItemDto> val shows: List<BaseItemDto>
) )
} }

View file

@ -8,7 +8,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.database.DownloadDatabaseDao
@ -18,11 +22,11 @@ import dev.jdtech.jellyfin.mpv.TrackType
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.AppPreferences import dev.jdtech.jellyfin.utils.AppPreferences
import dev.jdtech.jellyfin.utils.postDownloadPlaybackProgress import dev.jdtech.jellyfin.utils.postDownloadPlaybackProgress
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PlayerActivityViewModel class PlayerActivityViewModel
@ -133,8 +137,8 @@ constructor(
player.setMediaItems(mediaItems, currentMediaItemIndex, items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET) player.setMediaItems(mediaItems, currentMediaItemIndex, items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET)
val useMpv = sp.getBoolean("mpv_player", false) val useMpv = sp.getBoolean("mpv_player", false)
if(!useMpv || !playFromDownloads) if (!useMpv || !playFromDownloads)
player.prepare() //TODO: This line causes a crash when playing from downloads with MPV player.prepare() // TODO: This line causes a crash when playing from downloads with MPV
player.play() player.play()
pollPosition(player) pollPosition(player)
} }
@ -167,8 +171,8 @@ constructor(
override fun run() { override fun run() {
viewModelScope.launch { viewModelScope.launch {
if (player.currentMediaItem != null && player.currentMediaItem!!.mediaId.isNotEmpty()) { if (player.currentMediaItem != null && player.currentMediaItem!!.mediaId.isNotEmpty()) {
if(playFromDownloads){ if (playFromDownloads) {
postDownloadPlaybackProgress(downloadDatabase, items[0].itemId, player.currentPosition, (player.currentPosition.toDouble()/player.duration.toDouble()).times(100)) //TODO Automatically use the correct item postDownloadPlaybackProgress(downloadDatabase, items[0].itemId, player.currentPosition, (player.currentPosition.toDouble() / player.duration.toDouble()).times(100)) // TODO Automatically use the correct item
} }
try { try {
jellyfinRepository.postPlaybackProgress( jellyfinRepository.postPlaybackProgress(
@ -265,4 +269,4 @@ constructor(
player.setPlaybackSpeed(speed) player.setPlaybackSpeed(speed)
playbackSpeed = speed playbackSpeed = speed
} }
} }

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