diff --git a/app/phone/build.gradle.kts b/app/phone/build.gradle.kts index ea85a0a1..d61a881f 100644 --- a/app/phone/build.gradle.kts +++ b/app/phone/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.core) + implementation(libs.androidx.hilt.work) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.media3.exoplayer) @@ -95,6 +96,7 @@ dependencies { implementation(libs.androidx.recyclerview.selection) implementation(libs.androidx.room.ktx) implementation(libs.androidx.swiperefreshlayout) + implementation(libs.androidx.work) implementation(libs.glide) implementation(libs.hilt.android) kapt(libs.hilt.compiler) diff --git a/app/phone/src/main/AndroidManifest.xml b/app/phone/src/main/AndroidManifest.xml index f1d16eb1..b7e06395 100644 --- a/app/phone/src/main/AndroidManifest.xml +++ b/app/phone/src/main/AndroidManifest.xml @@ -1,10 +1,14 @@ - + + - + + + + + + + + + \ No newline at end of file diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/BaseApplication.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/BaseApplication.kt index 2d7a78dc..de37337a 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/BaseApplication.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/BaseApplication.kt @@ -2,16 +2,26 @@ package dev.jdtech.jellyfin import android.app.Application import androidx.appcompat.app.AppCompatDelegate +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject import timber.log.Timber @HiltAndroidApp -class BaseApplication : Application() { +class BaseApplication : Application(), Configuration.Provider { @Inject lateinit var appPreferences: AppPreferences + @Inject + lateinit var workerFactory: HiltWorkerFactory + + override fun getWorkManagerConfiguration() = + Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + override fun onCreate() { super.onCreate() diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index 71c5014e..2ba62bcc 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -12,6 +12,9 @@ import dev.jdtech.jellyfin.adapters.ServerGridAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.Server import dev.jdtech.jellyfin.models.User import java.util.UUID @@ -27,7 +30,7 @@ fun bindServers(recyclerView: RecyclerView, data: List?) { } @BindingAdapter("items") -fun bindItems(recyclerView: RecyclerView, data: List?) { +fun bindItems(recyclerView: RecyclerView, data: List?) { val adapter = recyclerView.adapter as ViewItemListAdapter adapter.submitList(data) } @@ -42,8 +45,21 @@ fun bindItemImage(imageView: ImageView, item: BaseItemDto) { .posterDescription(item.name) } +@BindingAdapter("itemImage") +fun bindItemImage(imageView: ImageView, item: FindroidItem) { + val itemId = when (item) { + is FindroidEpisode -> item.seriesId +// is JellyfinSeasonItem && item.imageTags.isNullOrEmpty() -> item.seriesId + else -> item.id + } + + imageView + .loadImage("/items/$itemId/Images/${ImageType.PRIMARY}") + .posterDescription(item.name) +} + @BindingAdapter("itemBackdropImage") -fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) { +fun bindItemBackdropImage(imageView: ImageView, item: FindroidItem?) { if (item == null) return imageView @@ -64,41 +80,21 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) { } @BindingAdapter("homeEpisodes") -fun bindHomeEpisodes(recyclerView: RecyclerView, data: List?) { +fun bindHomeEpisodes(recyclerView: RecyclerView, data: List?) { val adapter = recyclerView.adapter as HomeEpisodeListAdapter adapter.submitList(data) } -@BindingAdapter("baseItemImage") -fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) { - if (episode == null) return - - var imageItemId = episode.id - var imageType = ImageType.PRIMARY - - if (!episode.imageTags.isNullOrEmpty()) { // TODO: Downloadmetadata currently does not store imagetags, so it always uses the backdrop - when (episode.type) { - BaseItemKind.MOVIE -> { - if (!episode.backdropImageTags.isNullOrEmpty()) { - imageType = ImageType.BACKDROP - } - } - else -> { - if (!episode.imageTags!!.keys.contains(ImageType.PRIMARY)) { - imageType = ImageType.BACKDROP - } - } - } - } else { - if (episode.type == BaseItemKind.EPISODE) { - imageItemId = episode.seriesId!! - imageType = ImageType.BACKDROP - } +@BindingAdapter("cardItemImage") +fun bindCardItemImage(imageView: ImageView, item: FindroidItem) { + val imageType = when (item) { + is FindroidMovie -> ImageType.BACKDROP + else -> ImageType.PRIMARY } imageView - .loadImage("/items/$imageItemId/Images/$imageType") - .posterDescription(episode.name) + .loadImage("/items/${item.id}/Images/$imageType") + .posterDescription(item.name) } @BindingAdapter("seasonPoster") diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index 46946c0c..603e03b5 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -1,9 +1,11 @@ package dev.jdtech.jellyfin import android.os.Bundle +import android.os.Environment import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavGraph import androidx.navigation.fragment.NavHostFragment @@ -11,14 +13,20 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUiSaveStateControl import androidx.navigation.ui.setupActionBarWithNavController +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import com.google.android.material.navigation.NavigationBarView import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.databinding.ActivityMainBinding -import dev.jdtech.jellyfin.utils.loadDownloadLocation import dev.jdtech.jellyfin.viewmodels.MainViewModel +import dev.jdtech.jellyfin.work.SyncWorker import javax.inject.Inject +import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -39,6 +47,26 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + print("OnCreate LOOOOOL") + + val syncWorkRequest = OneTimeWorkRequestBuilder() + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType( + NetworkType.CONNECTED + ) + .build() + ) + .build() + + val workManager = WorkManager.getInstance(applicationContext) + + workManager.beginUniqueWork("syncUserData", ExistingWorkPolicy.KEEP, syncWorkRequest).enqueue() + + if (!appPreferences.downloadsMigrated) { + cleanUpOldDownloads() + } + if (appPreferences.amoledTheme) { setTheme(CoreR.style.Theme_FindroidAMOLED) } @@ -62,6 +90,11 @@ class MainActivity : AppCompatActivity() { val navView: NavigationBarView = binding.navView as NavigationBarView + if (appPreferences.offlineMode) { + navView.menu.clear() + navView.inflateMenu(CoreR.menu.bottom_nav_menu_offline) + } + setSupportActionBar(binding.mainToolbar) // Passing each menu ID as a set of Ids because each @@ -71,7 +104,7 @@ class MainActivity : AppCompatActivity() { R.id.homeFragment, R.id.mediaFragment, R.id.favoriteFragment, - R.id.downloadFragment + R.id.downloadsFragment, ) ) @@ -88,8 +121,6 @@ class MainActivity : AppCompatActivity() { if (destination.id == com.mikepenz.aboutlibraries.R.id.about_libraries_dest) binding.mainToolbar.title = getString(CoreR.string.app_info) } - - loadDownloadLocation(applicationContext) } override fun onSupportNavigateUp(): Boolean { @@ -119,4 +150,25 @@ class MainActivity : AppCompatActivity() { } } } + + /** + * Temp to remove old downloads, will be removed in a future version + */ + private fun cleanUpOldDownloads() { + lifecycleScope.launch { + val oldDir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_MOVIES) + if (oldDir == null) { + appPreferences.downloadsMigrated = true + return@launch + } + + try { + for (file in oldDir.listFiles()!!) { + file.delete() + } + } catch (_: Exception) {} + + appPreferences.downloadsMigrated = true + } + } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/CollectionListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/CollectionListAdapter.kt index 61c4cd52..b9553773 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/CollectionListAdapter.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/CollectionListAdapter.kt @@ -6,25 +6,25 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.jdtech.jellyfin.databinding.CollectionItemBinding -import org.jellyfin.sdk.model.api.BaseItemDto +import dev.jdtech.jellyfin.models.FindroidCollection class CollectionListAdapter( private val onClickListener: OnClickListener -) : ListAdapter(DiffCallback) { +) : ListAdapter(DiffCallback) { class CollectionViewHolder(private var binding: CollectionItemBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(collection: BaseItemDto) { + fun bind(collection: FindroidCollection) { binding.collection = collection binding.executePendingBindings() } } - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FindroidCollection, newItem: FindroidCollection): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + override fun areContentsTheSame(oldItem: FindroidCollection, newItem: FindroidCollection): Boolean { return oldItem == newItem } } @@ -47,7 +47,7 @@ class CollectionListAdapter( holder.bind(collection) } - class OnClickListener(val clickListener: (collection: BaseItemDto) -> Unit) { - fun onClick(collection: BaseItemDto) = clickListener(collection) + class OnClickListener(val clickListener: (collection: FindroidCollection) -> Unit) { + fun onClick(collection: FindroidCollection) = clickListener(collection) } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt deleted file mode 100644 index 793c492b..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt +++ /dev/null @@ -1,116 +0,0 @@ -package dev.jdtech.jellyfin.adapters - -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import dev.jdtech.jellyfin.databinding.EpisodeItemBinding -import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding -import dev.jdtech.jellyfin.models.DownloadEpisodeItem -import dev.jdtech.jellyfin.models.DownloadSeriesMetadata -import dev.jdtech.jellyfin.models.PlayerItem -import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto -import org.jellyfin.sdk.model.api.BaseItemDto - -private const val ITEM_VIEW_TYPE_HEADER = 0 -private const val ITEM_VIEW_TYPE_EPISODE = 1 - -class DownloadEpisodeListAdapter( - private val onClickListener: OnClickListener, - private val downloadSeriesMetadata: DownloadSeriesMetadata -) : - ListAdapter(DiffCallback) { - - class HeaderViewHolder(private var binding: SeasonHeaderBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind( - metadata: DownloadSeriesMetadata - ) { - binding.seasonName.text = metadata.name - binding.seriesId = metadata.itemId - binding.seasonId = metadata.itemId - binding.executePendingBindings() - } - } - - class EpisodeViewHolder(private var binding: EpisodeItemBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(episode: BaseItemDto) { - binding.episode = episode - if (episode.userData?.playedPercentage != null) { - binding.progressBar.layoutParams.width = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - (episode.userData?.playedPercentage?.times(.84))!!.toFloat(), - binding.progressBar.context.resources.displayMetrics - ).toInt() - binding.progressBar.visibility = View.VISIBLE - } else { - binding.progressBar.visibility = View.GONE - } - binding.executePendingBindings() - } - } - - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: DownloadEpisodeItem, newItem: DownloadEpisodeItem): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: DownloadEpisodeItem, newItem: DownloadEpisodeItem): Boolean { - return oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - ITEM_VIEW_TYPE_HEADER -> { - HeaderViewHolder( - SeasonHeaderBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - ITEM_VIEW_TYPE_EPISODE -> { - EpisodeViewHolder( - EpisodeItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - else -> throw ClassCastException("Unknown viewType $viewType") - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder.itemViewType) { - ITEM_VIEW_TYPE_HEADER -> { - (holder as HeaderViewHolder).bind(downloadSeriesMetadata) - } - ITEM_VIEW_TYPE_EPISODE -> { - val item = getItem(position) as DownloadEpisodeItem.Episode - holder.itemView.setOnClickListener { - onClickListener.onClick(item.episode) - } - (holder as EpisodeViewHolder).bind(downloadMetadataToBaseItemDto(item.episode.item!!)) - } - } - } - - override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is DownloadEpisodeItem.Header -> ITEM_VIEW_TYPE_HEADER - is DownloadEpisodeItem.Episode -> ITEM_VIEW_TYPE_EPISODE - } - } - - class OnClickListener(val clickListener: (item: PlayerItem) -> Unit) { - fun onClick(item: PlayerItem) = clickListener(item) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadSeriesListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadSeriesListAdapter.kt deleted file mode 100644 index 650bd285..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadSeriesListAdapter.kt +++ /dev/null @@ -1,71 +0,0 @@ -package dev.jdtech.jellyfin.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import dev.jdtech.jellyfin.core.R as CoreR -import dev.jdtech.jellyfin.databinding.BaseItemBinding -import dev.jdtech.jellyfin.models.DownloadSeriesMetadata -import dev.jdtech.jellyfin.utils.downloadSeriesMetadataToBaseItemDto - -class DownloadSeriesListAdapter( - private val onClickListener: OnClickListener, - private val fixedWidth: Boolean = false, -) : ListAdapter(DiffCallback) { - - class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : - RecyclerView.ViewHolder(binding.root) { - fun bind(item: DownloadSeriesMetadata, fixedWidth: Boolean) { - binding.item = downloadSeriesMetadataToBaseItemDto(item) - binding.itemName.text = item.name - binding.itemCount.text = item.episodes.size.toString() - if (fixedWidth) { - binding.itemLayout.layoutParams.width = - parent.resources.getDimension(CoreR.dimen.overview_media_width).toInt() - (binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0 - } - binding.executePendingBindings() - } - } - - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: DownloadSeriesMetadata, - newItem: DownloadSeriesMetadata - ): Boolean { - return oldItem.itemId == newItem.itemId - } - - override fun areContentsTheSame( - oldItem: DownloadSeriesMetadata, - newItem: DownloadSeriesMetadata - ): Boolean { - return oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { - return ItemViewHolder( - BaseItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - parent - ) - } - - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - val item = getItem(position) - holder.itemView.setOnClickListener { - onClickListener.onClick(item) - } - holder.bind(item, fixedWidth) - } - - class OnClickListener(val clickListener: (item: DownloadSeriesMetadata) -> Unit) { - fun onClick(item: DownloadSeriesMetadata) = clickListener(item) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt deleted file mode 100644 index d57795f3..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package dev.jdtech.jellyfin.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import dev.jdtech.jellyfin.core.R as CoreR -import dev.jdtech.jellyfin.databinding.BaseItemBinding -import dev.jdtech.jellyfin.models.PlayerItem -import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto - -class DownloadViewItemListAdapter( - private val onClickListener: OnClickListener, - private val fixedWidth: Boolean = false, -) : ListAdapter(DiffCallback) { - - class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : - RecyclerView.ViewHolder(binding.root) { - fun bind(item: PlayerItem, fixedWidth: Boolean) { - val metadata = item.item!! - binding.item = downloadMetadataToBaseItemDto(metadata) - binding.itemName.text = item.name - binding.itemCount.visibility = View.GONE - if (fixedWidth) { - binding.itemLayout.layoutParams.width = parent.resources.getDimension(CoreR.dimen.overview_media_width).toInt() - (binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0 - } - binding.executePendingBindings() - } - } - - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: PlayerItem, newItem: PlayerItem): Boolean { - return oldItem.itemId == newItem.itemId - } - - override fun areContentsTheSame(oldItem: PlayerItem, newItem: PlayerItem): Boolean { - return oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { - return ItemViewHolder( - BaseItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - parent - ) - } - - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - val item = getItem(position) - holder.itemView.setOnClickListener { - onClickListener.onClick(item) - } - holder.bind(item, fixedWidth) - } - - class OnClickListener(val clickListener: (item: PlayerItem) -> Unit) { - fun onClick(item: PlayerItem) = clickListener(item) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadsListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadsListAdapter.kt deleted file mode 100644 index 9f9f819b..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/DownloadsListAdapter.kt +++ /dev/null @@ -1,64 +0,0 @@ -package dev.jdtech.jellyfin.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import dev.jdtech.jellyfin.databinding.DownloadSectionBinding -import dev.jdtech.jellyfin.models.DownloadSection - -class DownloadsListAdapter( - private val onClickListener: DownloadViewItemListAdapter.OnClickListener, - private val onSeriesClickListener: DownloadSeriesListAdapter.OnClickListener -) : ListAdapter(DiffCallback) { - class SectionViewHolder(private var binding: DownloadSectionBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind( - section: DownloadSection, - onClickListener: DownloadViewItemListAdapter.OnClickListener, - onSeriesClickListener: DownloadSeriesListAdapter.OnClickListener - ) { - binding.section = section - when (section.name) { - "Movies" -> { - binding.itemsRecyclerView.adapter = DownloadViewItemListAdapter(onClickListener, fixedWidth = true) - (binding.itemsRecyclerView.adapter as DownloadViewItemListAdapter).submitList(section.items) - } - "Shows" -> { - binding.itemsRecyclerView.adapter = DownloadSeriesListAdapter(onSeriesClickListener, fixedWidth = true) - (binding.itemsRecyclerView.adapter as DownloadSeriesListAdapter).submitList(section.series) - } - } - binding.executePendingBindings() - } - } - - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: DownloadSection, newItem: DownloadSection): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame( - oldItem: DownloadSection, - newItem: DownloadSection - ): Boolean { - return oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder { - return SectionViewHolder( - DownloadSectionBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder(holder: SectionViewHolder, position: Int) { - val collection = getItem(position) - holder.bind(collection, onClickListener, onSeriesClickListener) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt index b425b552..33ade90c 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt @@ -4,57 +4,52 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.jdtech.jellyfin.databinding.EpisodeItemBinding import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding import dev.jdtech.jellyfin.models.EpisodeItem -import java.util.UUID -import org.jellyfin.sdk.model.api.BaseItemDto +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.isDownloaded private const val ITEM_VIEW_TYPE_HEADER = 0 private const val ITEM_VIEW_TYPE_EPISODE = 1 class EpisodeListAdapter( private val onClickListener: OnClickListener, - private val seriesId: UUID, - private val seriesName: String?, - private val seasonId: UUID, - private val seasonName: String? ) : ListAdapter(DiffCallback) { class HeaderViewHolder(private var binding: SeasonHeaderBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind( - seriesId: UUID, - seriesName: String?, - seasonId: UUID, - seasonName: String? - ) { - binding.seriesId = seriesId - binding.seasonId = seasonId - binding.seasonName.text = seasonName - binding.seriesName.text = seriesName + fun bind(header: EpisodeItem.Header) { + binding.seriesId = header.seriesId + binding.seasonId = header.seasonId + binding.seasonName.text = header.seasonName + binding.seriesName.text = header.seriesName binding.executePendingBindings() } } class EpisodeViewHolder(private var binding: EpisodeItemBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(episode: BaseItemDto) { + fun bind(episode: FindroidEpisode) { binding.episode = episode - if (episode.userData?.playedPercentage != null) { + if (episode.playbackPositionTicks > 0) { binding.progressBar.layoutParams.width = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, - (episode.userData?.playedPercentage?.times(.84))!!.toFloat(), + (episode.playbackPositionTicks.div(episode.runtimeTicks.toFloat()).times(84)), binding.progressBar.context.resources.displayMetrics ).toInt() binding.progressBar.visibility = View.VISIBLE } else { binding.progressBar.visibility = View.GONE } + + binding.downloadedIcon.isVisible = episode.isDownloaded() + binding.executePendingBindings() } } @@ -96,7 +91,8 @@ class EpisodeListAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder.itemViewType) { ITEM_VIEW_TYPE_HEADER -> { - (holder as HeaderViewHolder).bind(seriesId, seriesName, seasonId, seasonName) + val item = getItem(position) as EpisodeItem.Header + (holder as HeaderViewHolder).bind(item) } ITEM_VIEW_TYPE_EPISODE -> { val item = getItem(position) as EpisodeItem.Episode @@ -115,7 +111,7 @@ class EpisodeListAdapter( } } - class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { - fun onClick(item: BaseItemDto) = clickListener(item) + class OnClickListener(val clickListener: (item: FindroidEpisode) -> Unit) { + fun onClick(item: FindroidEpisode) = clickListener(item) } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/HomeEpisodeListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/HomeEpisodeListAdapter.kt index 42a83f01..82be6498 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/HomeEpisodeListAdapter.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/HomeEpisodeListAdapter.kt @@ -4,44 +4,57 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.isDownloaded -class HomeEpisodeListAdapter(private val onClickListener: OnClickListener) : ListAdapter(DiffCallback) { - class EpisodeViewHolder(private var binding: HomeEpisodeItemBinding) : +class HomeEpisodeListAdapter(private val onClickListener: OnClickListener) : ListAdapter(DiffCallback) { + class EpisodeViewHolder( + private var binding: HomeEpisodeItemBinding, + private val parent: ViewGroup + ) : RecyclerView.ViewHolder(binding.root) { - fun bind(episode: BaseItemDto) { - binding.episode = episode - if (episode.userData?.playedPercentage != null) { + fun bind(item: FindroidItem) { + binding.item = item + if (item.playbackPositionTicks > 0) { binding.progressBar.layoutParams.width = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, - (episode.userData?.playedPercentage?.times(2.24))!!.toFloat(), binding.progressBar.context.resources.displayMetrics + (item.playbackPositionTicks.div(item.runtimeTicks.toFloat()).times(224)), binding.progressBar.context.resources.displayMetrics ).toInt() binding.progressBar.visibility = View.VISIBLE } - if (episode.type == BaseItemKind.MOVIE) { - binding.primaryName.text = episode.name - binding.secondaryName.visibility = View.GONE - } else if (episode.type == BaseItemKind.EPISODE) { - binding.primaryName.text = episode.seriesName + binding.downloadedIcon.isVisible = item.isDownloaded() + + when (item) { + is FindroidMovie -> { + binding.primaryName.text = item.name + binding.secondaryName.visibility = View.GONE + } + is FindroidEpisode -> { + binding.primaryName.text = item.seriesName + binding.secondaryName.text = parent.resources.getString(CoreR.string.episode_name_extended, item.parentIndexNumber, item.indexNumber, item.name) + } } binding.executePendingBindings() } } - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FindroidItem, newItem: FindroidItem): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { - return oldItem == newItem + override fun areContentsTheSame(oldItem: FindroidItem, newItem: FindroidItem): Boolean { + return oldItem.name == newItem.name } } @@ -51,7 +64,8 @@ class HomeEpisodeListAdapter(private val onClickListener: OnClickListener) : Lis LayoutInflater.from(parent.context), parent, false - ) + ), + parent ) } @@ -63,7 +77,7 @@ class HomeEpisodeListAdapter(private val onClickListener: OnClickListener) : Lis holder.bind(item) } - class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { - fun onClick(item: BaseItemDto) = clickListener(item) + class OnClickListener(val clickListener: (item: FindroidItem) -> Unit) { + fun onClick(item: FindroidItem) = clickListener(item) } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemListAdapter.kt index b2ff7cbd..afa1d4a1 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemListAdapter.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemListAdapter.kt @@ -3,42 +3,47 @@ package dev.jdtech.jellyfin.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.BaseItemBinding -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.isDownloaded class ViewItemListAdapter( private val onClickListener: OnClickListener, private val fixedWidth: Boolean = false, -) : ListAdapter(DiffCallback) { +) : ListAdapter(DiffCallback) { class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: BaseItemDto, fixedWidth: Boolean) { + fun bind(item: FindroidItem, fixedWidth: Boolean) { binding.item = item - binding.itemName.text = if (item.type == BaseItemKind.EPISODE) item.seriesName else item.name + binding.itemName.text = if (item is FindroidEpisode) item.seriesName else item.name binding.itemCount.visibility = - if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE + if (item.unplayedItemCount != null && item.unplayedItemCount!! > 0) View.VISIBLE else View.GONE if (fixedWidth) { binding.itemLayout.layoutParams.width = parent.resources.getDimension(CoreR.dimen.overview_media_width).toInt() (binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0 } + + binding.downloadedIcon.isVisible = item.isDownloaded() + binding.executePendingBindings() } } - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FindroidItem, newItem: FindroidItem): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { - return oldItem == newItem + override fun areContentsTheSame(oldItem: FindroidItem, newItem: FindroidItem): Boolean { + return oldItem.name == newItem.name } } @@ -61,7 +66,7 @@ class ViewItemListAdapter( holder.bind(item, fixedWidth) } - class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { - fun onClick(item: BaseItemDto) = clickListener(item) + class OnClickListener(val clickListener: (item: FindroidItem) -> Unit) { + fun onClick(item: FindroidItem) = clickListener(item) } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemPagingAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemPagingAdapter.kt index 37e9705d..b9a374db 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemPagingAdapter.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewItemPagingAdapter.kt @@ -3,43 +3,48 @@ package dev.jdtech.jellyfin.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.BaseItemBinding -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.isDownloaded class ViewItemPagingAdapter( private val onClickListener: OnClickListener, private val fixedWidth: Boolean = false, -) : PagingDataAdapter(DiffCallback) { +) : PagingDataAdapter(DiffCallback) { class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: BaseItemDto, fixedWidth: Boolean) { + fun bind(item: FindroidItem, fixedWidth: Boolean) { binding.item = item binding.itemName.text = - if (item.type == BaseItemKind.EPISODE) item.seriesName else item.name + if (item is FindroidEpisode) item.seriesName else item.name binding.itemCount.visibility = - if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE + if (item.unplayedItemCount != null && item.unplayedItemCount!! > 0) View.VISIBLE else View.GONE if (fixedWidth) { binding.itemLayout.layoutParams.width = parent.resources.getDimension(CoreR.dimen.overview_media_width).toInt() (binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0 } + + binding.downloadedIcon.isVisible = item.isDownloaded() + binding.executePendingBindings() } } - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FindroidItem, newItem: FindroidItem): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { - return oldItem == newItem + override fun areContentsTheSame(oldItem: FindroidItem, newItem: FindroidItem): Boolean { + return oldItem.name == newItem.name } } @@ -64,7 +69,7 @@ class ViewItemPagingAdapter( } } - class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { - fun onClick(item: BaseItemDto) = clickListener(item) + class OnClickListener(val clickListener: (item: FindroidItem) -> Unit) { + fun onClick(item: FindroidItem) = clickListener(item) } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt index 75787a5d..6eeff900 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt @@ -6,6 +6,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.databinding.CardOfflineBinding import dev.jdtech.jellyfin.databinding.NextUpSectionBinding import dev.jdtech.jellyfin.databinding.ViewItemBinding import dev.jdtech.jellyfin.models.HomeItem @@ -13,11 +14,13 @@ import dev.jdtech.jellyfin.models.View private const val ITEM_VIEW_TYPE_NEXT_UP = 0 private const val ITEM_VIEW_TYPE_VIEW = 1 +private const val ITEM_VIEW_TYPE_OFFLINE_CARD = 2 class ViewListAdapter( private val onClickListener: OnClickListener, private val onItemClickListener: ViewItemListAdapter.OnClickListener, - private val onNextUpClickListener: HomeEpisodeListAdapter.OnClickListener + private val onNextUpClickListener: HomeEpisodeListAdapter.OnClickListener, + private val onOnlineClickListener: OnClickListenerOfflineCard ) : ListAdapter(DiffCallback) { class ViewViewHolder(private var binding: ViewItemBinding) : @@ -43,11 +46,20 @@ class ViewListAdapter( RecyclerView.ViewHolder(binding.root) { fun bind(section: HomeItem.Section, onClickListener: HomeEpisodeListAdapter.OnClickListener) { binding.section = section.homeSection + binding.sectionName.text = section.homeSection.name.asString(binding.sectionName.context.resources) binding.itemsRecyclerView.adapter = HomeEpisodeListAdapter(onClickListener) binding.executePendingBindings() } } + class OfflineCardViewHolder(private var binding: CardOfflineBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(onClickListener: OnClickListenerOfflineCard) { + binding.onlineButton.setOnClickListener { + onClickListener.onClick() + } + } + } + companion object DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean { return oldItem.id == newItem.id @@ -75,6 +87,15 @@ class ViewListAdapter( false ) ) + ITEM_VIEW_TYPE_OFFLINE_CARD -> { + OfflineCardViewHolder( + CardOfflineBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } else -> throw ClassCastException("Unknown viewType $viewType") } } @@ -89,11 +110,15 @@ class ViewListAdapter( val view = getItem(position) as HomeItem.ViewItem (holder as ViewViewHolder).bind(view, onClickListener, onItemClickListener) } + ITEM_VIEW_TYPE_OFFLINE_CARD -> { + (holder as OfflineCardViewHolder).bind(onOnlineClickListener) + } } } override fun getItemViewType(position: Int): Int { return when (getItem(position)) { + is HomeItem.OfflineCard -> ITEM_VIEW_TYPE_OFFLINE_CARD is HomeItem.Libraries -> -1 is HomeItem.Section -> ITEM_VIEW_TYPE_NEXT_UP is HomeItem.ViewItem -> ITEM_VIEW_TYPE_VIEW @@ -103,4 +128,8 @@ class ViewListAdapter( class OnClickListener(val clickListener: (view: View) -> Unit) { fun onClick(view: View) = clickListener(view) } + + class OnClickListenerOfflineCard(val clickListener: () -> Unit) { + fun onClick() = clickListener() + } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/dialogs/StorageSelectionDialog.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/dialogs/StorageSelectionDialog.kt new file mode 100644 index 00000000..61e646aa --- /dev/null +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/dialogs/StorageSelectionDialog.kt @@ -0,0 +1,29 @@ +package dev.jdtech.jellyfin.dialogs + +import android.content.Context +import android.os.Environment +import android.os.StatFs +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dev.jdtech.jellyfin.core.R as CoreR + +fun getStorageSelectionDialog( + context: Context, + onItemSelected: (which: Int) -> Unit, + onCancel: () -> Unit, +): AlertDialog { + val locations = context.getExternalFilesDirs(null).mapNotNull { + val locationStringRes = if (Environment.isExternalStorageRemovable(it)) CoreR.string.external else CoreR.string.internal + val stat = StatFs(it.path) + context.getString(CoreR.string.storage_name, context.getString(locationStringRes), stat.availableBytes.div(1000000)) + }.toTypedArray() + val dialog = MaterialAlertDialogBuilder(context) + .setTitle(CoreR.string.select_storage_location) + .setItems(locations) { _, which -> + onItemSelected(which) + } + .setOnCancelListener() { + onCancel() + }.create() + return dialog +} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt index 6b8a1d14..e4a859a8 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt @@ -18,10 +18,13 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.CollectionViewModel import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber @AndroidEntryPoint @@ -41,10 +44,10 @@ class CollectionFragment : Fragment() { binding.favoritesRecyclerView.adapter = FavoritesListAdapter( ViewItemListAdapter.OnClickListener { item -> - navigateToMediaInfoFragment(item) + navigateToMediaItem(item) }, HomeEpisodeListAdapter.OnClickListener { item -> - navigateToEpisodeBottomSheetFragment(item) + navigateToMediaItem(item) } ) @@ -103,21 +106,31 @@ class CollectionFragment : Fragment() { checkIfLoginRequired(uiState.error.message) } - private fun navigateToMediaInfoFragment(item: BaseItemDto) { - findNavController().navigate( - CollectionFragmentDirections.actionCollectionFragmentToMediaInfoFragment( - item.id, - item.name, - item.type - ) - ) - } - - private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { - findNavController().navigate( - CollectionFragmentDirections.actionCollectionFragmentToEpisodeBottomSheetFragment( - episode.id - ) - ) + private fun navigateToMediaItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + CollectionFragmentDirections.actionCollectionFragmentToMovieFragment( + item.id, + item.name + ) + ) + } + is FindroidShow -> { + findNavController().navigate( + CollectionFragmentDirections.actionCollectionFragmentToShowFragment( + item.id, + item.name + ) + ) + } + is FindroidEpisode -> { + findNavController().navigate( + CollectionFragmentDirections.actionCollectionFragmentToEpisodeBottomSheetFragment( + item.id + ) + ) + } + } } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt deleted file mode 100644 index 2f286786..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt +++ /dev/null @@ -1,117 +0,0 @@ -package dev.jdtech.jellyfin.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import dagger.hilt.android.AndroidEntryPoint -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.dialogs.ErrorDialogFragment -import dev.jdtech.jellyfin.models.DownloadSeriesMetadata -import dev.jdtech.jellyfin.models.PlayerItem -import dev.jdtech.jellyfin.utils.checkIfLoginRequired -import dev.jdtech.jellyfin.viewmodels.DownloadViewModel -import java.util.UUID -import kotlinx.coroutines.launch -import timber.log.Timber - -@AndroidEntryPoint -class DownloadFragment : Fragment() { - - private lateinit var binding: FragmentDownloadBinding - private val viewModel: DownloadViewModel by viewModels() - - private lateinit var errorDialog: ErrorDialogFragment - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentDownloadBinding.inflate(inflater, container, false) - - binding.downloadsRecyclerView.adapter = - DownloadsListAdapter( - DownloadViewItemListAdapter.OnClickListener { item -> - navigateToMediaInfoFragment(item) - }, - DownloadSeriesListAdapter.OnClickListener { item -> - navigateToDownloadSeriesFragment(item) - } - ) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { uiState -> - Timber.d("$uiState") - when (uiState) { - is DownloadViewModel.UiState.Normal -> bindUiStateNormal(uiState) - is DownloadViewModel.UiState.Loading -> bindUiStateLoading() - is DownloadViewModel.UiState.Error -> bindUiStateError(uiState) - } - } - } - } - - binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadData() - } - - binding.errorLayout.errorDetailsButton.setOnClickListener { - errorDialog.show(parentFragmentManager, ErrorDialogFragment.TAG) - } - - return binding.root - } - - private fun bindUiStateNormal(uiState: DownloadViewModel.UiState.Normal) { - uiState.apply { - binding.noDownloadsText.isVisible = downloadSections.isEmpty() - - val adapter = binding.downloadsRecyclerView.adapter as DownloadsListAdapter - adapter.submitList(downloadSections) - } - binding.loadingIndicator.isVisible = false - binding.downloadsRecyclerView.isVisible = true - binding.errorLayout.errorPanel.isVisible = false - } - - private fun bindUiStateLoading() { - binding.loadingIndicator.isVisible = true - binding.errorLayout.errorPanel.isVisible = false - } - - private fun bindUiStateError(uiState: DownloadViewModel.UiState.Error) { - errorDialog = ErrorDialogFragment.newInstance(uiState.error) - binding.loadingIndicator.isVisible = false - binding.downloadsRecyclerView.isVisible = false - binding.errorLayout.errorPanel.isVisible = true - checkIfLoginRequired(uiState.error.message) - } - - private fun navigateToMediaInfoFragment(item: PlayerItem) { - findNavController().navigate( - DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment( - UUID.randomUUID(), item.name, item.item!!.type, item, isOffline = true - ) - ) - } - - private fun navigateToDownloadSeriesFragment(series: DownloadSeriesMetadata) { - findNavController().navigate( - DownloadFragmentDirections.actionDownloadFragmentToDownloadSeriesFragment( - seriesMetadata = series, seriesName = series.name - ) - ) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadSeriesFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadSeriesFragment.kt deleted file mode 100644 index 8d45c333..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadSeriesFragment.kt +++ /dev/null @@ -1,101 +0,0 @@ -package dev.jdtech.jellyfin.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import dev.jdtech.jellyfin.adapters.DownloadEpisodeListAdapter -import dev.jdtech.jellyfin.databinding.FragmentDownloadSeriesBinding -import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment -import dev.jdtech.jellyfin.models.PlayerItem -import dev.jdtech.jellyfin.viewmodels.DownloadSeriesViewModel -import java.util.UUID -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class DownloadSeriesFragment : Fragment() { - - private lateinit var binding: FragmentDownloadSeriesBinding - private val viewModel: DownloadSeriesViewModel by viewModels() - - private lateinit var errorDialog: ErrorDialogFragment - - private val args: DownloadSeriesFragmentArgs by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentDownloadSeriesBinding.inflate(inflater, container, false) - binding.lifecycleOwner = viewLifecycleOwner - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.viewModel = viewModel - - binding.episodesRecyclerView.adapter = - DownloadEpisodeListAdapter( - DownloadEpisodeListAdapter.OnClickListener { episode -> - navigateToEpisodeBottomSheetFragment(episode) - }, - args.seriesMetadata - ) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { uiState -> - when (uiState) { - is DownloadSeriesViewModel.UiState.Normal -> bindUiStateNormal(uiState) - is DownloadSeriesViewModel.UiState.Loading -> Unit - is DownloadSeriesViewModel.UiState.Error -> bindUiStateError(uiState) - } - } - } - } - - binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadEpisodes(args.seriesMetadata) - } - - binding.errorLayout.errorDetailsButton.setOnClickListener { - errorDialog.show(parentFragmentManager, ErrorDialogFragment.TAG) - } - - viewModel.loadEpisodes(args.seriesMetadata) - } - - private fun bindUiStateNormal(uiState: DownloadSeriesViewModel.UiState.Normal) { - val adapter = binding.episodesRecyclerView.adapter as DownloadEpisodeListAdapter - adapter.submitList(uiState.downloadEpisodes) - binding.episodesRecyclerView.isVisible = true - binding.errorLayout.errorPanel.isVisible = false - } - - private fun bindUiStateError(uiState: DownloadSeriesViewModel.UiState.Error) { - errorDialog = ErrorDialogFragment.newInstance(uiState.error) - binding.episodesRecyclerView.isVisible = false - binding.errorLayout.errorPanel.isVisible = true - } - - private fun navigateToEpisodeBottomSheetFragment(episode: PlayerItem) { - findNavController().navigate( - DownloadSeriesFragmentDirections.actionDownloadSeriesFragmentToEpisodeBottomSheetFragment( - UUID.randomUUID(), - episode, - isOffline = true - ) - ) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadsFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadsFragment.kt new file mode 100644 index 00000000..2ce4edec --- /dev/null +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/DownloadsFragment.kt @@ -0,0 +1,126 @@ +package dev.jdtech.jellyfin.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.AppPreferences +import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.adapters.FavoritesListAdapter +import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter +import dev.jdtech.jellyfin.adapters.ViewItemListAdapter +import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.databinding.FragmentDownloadsBinding +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow +import dev.jdtech.jellyfin.utils.restart +import dev.jdtech.jellyfin.viewmodels.DownloadsViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint +class DownloadsFragment : Fragment() { + private lateinit var binding: FragmentDownloadsBinding + private val viewModel: DownloadsViewModel by viewModels() + + @Inject + lateinit var appPreferences: AppPreferences + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentDownloadsBinding.inflate(inflater, container, false) + + binding.downloadsRecyclerView.adapter = FavoritesListAdapter( + ViewItemListAdapter.OnClickListener { item -> + navigateToMediaItem(item) + }, + HomeEpisodeListAdapter.OnClickListener { item -> + navigateToMediaItem(item) + } + ) + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.connectionError.collect { + Snackbar.make(binding.root, CoreR.string.no_server_connection, Snackbar.LENGTH_INDEFINITE) + .setAnchorView(requireActivity().findViewById(R.id.nav_view)) + .setAction(CoreR.string.offline_mode) { + appPreferences.offlineMode = true + activity?.restart() + } + .show() + } + } + launch { + viewModel.uiState.collect { uiState -> + Timber.d("$uiState") + when (uiState) { + is DownloadsViewModel.UiState.Normal -> bindUiStateNormal(uiState) + is DownloadsViewModel.UiState.Loading -> bindUiStateLoading() + is DownloadsViewModel.UiState.Error -> Unit + } + } + } + } + } + + return binding.root + } + + override fun onResume() { + super.onResume() + + viewModel.loadData() + } + + private fun bindUiStateNormal(uiState: DownloadsViewModel.UiState.Normal) { + binding.loadingIndicator.isVisible = false + binding.downloadsRecyclerView.isVisible = true + binding.errorLayout.errorPanel.isVisible = false + binding.noDownloadsText.isVisible = uiState.sections.isEmpty() + val adapter = binding.downloadsRecyclerView.adapter as FavoritesListAdapter + adapter.submitList(uiState.sections) + } + + private fun bindUiStateLoading() { + binding.loadingIndicator.isVisible = true + binding.errorLayout.errorPanel.isVisible = false + } + + private fun navigateToMediaItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + DownloadsFragmentDirections.actionDownloadsFragmentToMovieFragment( + item.id, + item.name + ) + ) + } + is FindroidShow -> { + findNavController().navigate( + DownloadsFragmentDirections.actionDownloadsFragmentToShowFragment( + item.id, + item.name, + true + ) + ) + } + } + } +} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 2311a3fc..6de4f47a 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -1,10 +1,13 @@ package dev.jdtech.jellyfin.fragments +import android.R as AndroidR +import android.app.DownloadManager import android.os.Bundle import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.fragment.app.viewModels @@ -17,20 +20,30 @@ import com.google.android.material.R as MaterialR import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint -import dev.jdtech.jellyfin.bindBaseItemImage +import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.bindCardItemImage import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.dialogs.getStorageSelectionDialog +import dev.jdtech.jellyfin.dialogs.getVideoVersionDialog +import dev.jdtech.jellyfin.models.FindroidSourceType import dev.jdtech.jellyfin.models.PlayerItem +import dev.jdtech.jellyfin.models.UiText +import dev.jdtech.jellyfin.models.isDownloaded +import dev.jdtech.jellyfin.models.isDownloading import dev.jdtech.jellyfin.utils.setTintColor import dev.jdtech.jellyfin.utils.setTintColorAttribute import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel import dev.jdtech.jellyfin.viewmodels.PlayerViewModel +import java.text.DateFormat +import java.time.ZoneOffset +import java.util.Date import java.util.UUID import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.LocationType +import org.jellyfin.sdk.model.DateTime import timber.log.Timber @AndroidEntryPoint @@ -41,6 +54,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { private val viewModel: EpisodeBottomSheetViewModel by viewModels() private val playerViewModel: PlayerViewModel by viewModels() + private lateinit var downloadPreparingDialog: AlertDialog + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -48,31 +63,67 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { ): View { binding = EpisodeBottomSheetBinding.inflate(inflater, container, false) - binding.playButton.setOnClickListener { - binding.playButton.setImageResource(android.R.color.transparent) - binding.progressCircular.isVisible = true - if (viewModel.canRetry) { - binding.playButton.isEnabled = false - viewModel.download() - return@setOnClickListener - } - viewModel.item?.let { - if (!args.isOffline) { - playerViewModel.loadPlayerItems(it) - } else { - playerViewModel.loadOfflinePlayerItems(viewModel.playerItems[0]) - } - } + binding.itemActions.playButton.setOnClickListener { + binding.itemActions.playButton.setImageResource(AndroidR.color.transparent) + binding.itemActions.progressCircular.isVisible = true + playerViewModel.loadPlayerItems(viewModel.item) } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { uiState -> - Timber.d("$uiState") - when (uiState) { - is EpisodeBottomSheetViewModel.UiState.Normal -> bindUiStateNormal(uiState) - is EpisodeBottomSheetViewModel.UiState.Loading -> bindUiStateLoading() - is EpisodeBottomSheetViewModel.UiState.Error -> bindUiStateError(uiState) + launch { + viewModel.uiState.collect { uiState -> + Timber.d("$uiState") + when (uiState) { + is EpisodeBottomSheetViewModel.UiState.Normal -> bindUiStateNormal(uiState) + is EpisodeBottomSheetViewModel.UiState.Loading -> bindUiStateLoading() + is EpisodeBottomSheetViewModel.UiState.Error -> bindUiStateError(uiState) + } + } + } + + launch { + viewModel.downloadStatus.collect { (status, progress) -> + when (status) { + 10 -> { + downloadPreparingDialog.dismiss() + } + DownloadManager.STATUS_PENDING -> { + binding.itemActions.downloadButton.setImageResource(AndroidR.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + } + DownloadManager.STATUS_RUNNING -> { + binding.itemActions.downloadButton.setImageResource(AndroidR.color.transparent) + binding.itemActions.progressDownload.isVisible = true + if (progress < 5) { + binding.itemActions.progressDownload.isIndeterminate = true + } else { + binding.itemActions.progressDownload.isIndeterminate = false + binding.itemActions.progressDownload.setProgressCompat(progress, true) + } + } + DownloadManager.STATUS_SUCCESSFUL -> { + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_trash) + binding.itemActions.progressDownload.isVisible = false + } + else -> { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + } + } + } + + launch { + viewModel.downloadError.collect { uiText -> + createErrorDialog(uiText) + } + } + + launch { + viewModel.navigateBack.collect { + if (it) findNavController().navigateUp() } } } @@ -85,59 +136,80 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } } - if (!args.isOffline) { - val episodeId: UUID = args.episodeId + binding.seriesName.setOnClickListener { + navigateToSeries(viewModel.item.seriesId, viewModel.item.seriesName) + } - binding.checkButton.setOnClickListener { - when (viewModel.played) { - true -> { - viewModel.markAsUnplayed(episodeId) - binding.checkButton.setTintColorAttribute(MaterialR.attr.colorOnSecondaryContainer, requireActivity().theme) - } - false -> { - viewModel.markAsPlayed(episodeId) - binding.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) - } - } - } + binding.itemActions.checkButton.setOnClickListener { + val played = viewModel.togglePlayed() + bindCheckButtonState(played) + } - binding.favoriteButton.setOnClickListener { - when (viewModel.favorite) { - true -> { - viewModel.unmarkAsFavorite(episodeId) - binding.favoriteButton.setImageResource(CoreR.drawable.ic_heart) - binding.favoriteButton.setTintColorAttribute(MaterialR.attr.colorOnSecondaryContainer, requireActivity().theme) - } - false -> { - viewModel.markAsFavorite(episodeId) - binding.favoriteButton.setImageResource(CoreR.drawable.ic_heart_filled) - binding.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) - } - } - } + binding.itemActions.favoriteButton.setOnClickListener { + val favorite = viewModel.toggleFavorite() + bindFavoriteButtonState(favorite) + } - binding.downloadButton.setOnClickListener { - binding.downloadButton.isEnabled = false - viewModel.download() - binding.downloadButton.setTintColor(CoreR.color.red, requireActivity().theme) - } - - viewModel.loadEpisode(episodeId) - } else { - val playerItem = args.playerItem!! - viewModel.loadEpisode(playerItem) - - binding.deleteButton.isVisible = true - - binding.deleteButton.setOnClickListener { + binding.itemActions.downloadButton.setOnClickListener { + if (viewModel.item.isDownloaded()) { viewModel.deleteEpisode() - dismiss() - findNavController().navigate(CoreR.id.downloadFragment) + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } else if (viewModel.item.isDownloading()) { + createCancelDialog() + } else { + binding.itemActions.downloadButton.setImageResource(android.R.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { + val storageDialog = getStorageSelectionDialog( + requireContext(), + onItemSelected = { storageIndex -> + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex, storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + ) + dialog.show() + return@getStorageSelectionDialog + } + createDownloadPreparingDialog() + viewModel.download(storageIndex = storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + ) + storageDialog.show() + return@setOnClickListener + } + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + ) + dialog.show() + return@setOnClickListener + } + createDownloadPreparingDialog() + viewModel.download() } - - binding.checkButton.isVisible = false - binding.favoriteButton.isVisible = false - binding.downloadButtonWrapper.isVisible = false } return binding.root @@ -150,50 +222,41 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } } + override fun onResume() { + super.onResume() + + viewModel.loadEpisode(args.episodeId) + } + private fun bindUiStateNormal(uiState: EpisodeBottomSheetViewModel.UiState.Normal) { uiState.apply { - if (episode.userData?.playedPercentage != null) { + val canDownload = episode.canDownload && episode.sources.any { it.type == FindroidSourceType.REMOTE } + val canDelete = episode.sources.any { it.type == FindroidSourceType.LOCAL } + + if (episode.playbackPositionTicks > 0) { binding.progressBar.layoutParams.width = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, - (episode.userData?.playedPercentage?.times(1.26))!!.toFloat(), + (episode.playbackPositionTicks.div(episode.runtimeTicks).times(1.26)).toFloat(), context?.resources?.displayMetrics ).toInt() binding.progressBar.isVisible = true } - val clickable = canPlay && (available || canRetry) - binding.playButton.isEnabled = clickable - binding.playButton.alpha = if (!clickable) 0.5F else 1.0F - binding.playButton.setImageResource(if (!canRetry) CoreR.drawable.ic_play else CoreR.drawable.ic_rotate_ccw) - if (!(available || canRetry)) { - binding.playButton.setImageResource(android.R.color.transparent) - binding.progressCircular.isVisible = true + val canPlay = episode.canPlay && episode.sources.isNotEmpty() + binding.itemActions.playButton.isEnabled = canPlay + binding.itemActions.playButton.alpha = if (!canPlay) 0.5F else 1.0F + + bindCheckButtonState(episode.played) + + bindFavoriteButtonState(episode.favorite) + + if (episode.isDownloaded()) { + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_trash) } - // Check icon - when (played) { - true -> binding.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) - false -> binding.checkButton.setTintColorAttribute(MaterialR.attr.colorOnSecondaryContainer, requireActivity().theme) - } - - // Favorite icon - val favoriteDrawable = when (favorite) { - true -> CoreR.drawable.ic_heart_filled - false -> CoreR.drawable.ic_heart - } - binding.favoriteButton.setImageResource(favoriteDrawable) - if (favorite) binding.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) - - when (canDownload) { - true -> { - binding.downloadButtonWrapper.isVisible = true - binding.downloadButton.isEnabled = !downloaded - - if (downloaded) binding.downloadButton.setTintColor(CoreR.color.red, requireActivity().theme) - } - false -> { - binding.downloadButtonWrapper.isVisible = false - } + when (canDownload || canDelete) { + true -> binding.itemActions.downloadButton.isVisible = true + false -> binding.itemActions.downloadButton.isVisible = false } binding.episodeName.text = getString( @@ -204,18 +267,13 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { ) binding.seriesName.text = episode.seriesName binding.overview.text = episode.overview - binding.year.text = dateString - binding.playtime.text = runTime + binding.year.text = formatDateTime(episode.premiereDate) + binding.playtime.text = getString(CoreR.string.runtime_minutes, episode.runtimeTicks.div(600000000)) binding.communityRating.isVisible = episode.communityRating != null binding.communityRating.text = episode.communityRating.toString() - binding.missingIcon.isVisible = episode.locationType == LocationType.VIRTUAL + binding.missingIcon.isVisible = false - binding.seriesName.setOnClickListener { - if (episode.seriesId != null) { - navigateToSeries(episode.seriesId!!, episode.seriesName) - } - } - bindBaseItemImage(binding.episodeImage, episode) + bindCardItemImage(binding.episodeImage, episode) } binding.loadingIndicator.isVisible = false } @@ -231,31 +289,92 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) { navigateToPlayerActivity(items.items.toTypedArray()) - binding.playButton.setImageDrawable( + binding.itemActions.playButton.setImageDrawable( ContextCompat.getDrawable( requireActivity(), CoreR.drawable.ic_play ) ) - binding.progressCircular.visibility = View.INVISIBLE + binding.itemActions.progressCircular.visibility = View.INVISIBLE + } + + private fun bindCheckButtonState(played: Boolean) { + when (played) { + true -> binding.itemActions.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) + false -> binding.itemActions.checkButton.setTintColorAttribute( + MaterialR.attr.colorOnSecondaryContainer, + requireActivity().theme + ) + } + } + + private fun bindFavoriteButtonState(favorite: Boolean) { + val favoriteDrawable = when (favorite) { + true -> CoreR.drawable.ic_heart_filled + false -> CoreR.drawable.ic_heart + } + binding.itemActions.favoriteButton.setImageResource(favoriteDrawable) + when (favorite) { + true -> binding.itemActions.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) + false -> binding.itemActions.favoriteButton.setTintColorAttribute( + MaterialR.attr.colorOnSecondaryContainer, + requireActivity().theme + ) + } } private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) { Timber.e(error.error.message) binding.playerItemsError.isVisible = true - binding.playButton.setImageDrawable( + binding.itemActions.playButton.setImageDrawable( ContextCompat.getDrawable( requireActivity(), CoreR.drawable.ic_play ) ) - binding.progressCircular.visibility = View.INVISIBLE + binding.itemActions.progressCircular.visibility = View.INVISIBLE binding.playerItemsErrorDetails.setOnClickListener { ErrorDialogFragment.newInstance(error.error).show(parentFragmentManager, ErrorDialogFragment.TAG) } } + private fun createErrorDialog(uiText: UiText) { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder + .setTitle(CoreR.string.downloading_error) + .setMessage(uiText.asString(requireContext().resources)) + .setPositiveButton(getString(CoreR.string.close)) { _, _ -> + } + builder.show() + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + + private fun createDownloadPreparingDialog() { + val builder = MaterialAlertDialogBuilder(requireContext()) + downloadPreparingDialog = builder + .setTitle(CoreR.string.preparing_download) + .setView(R.layout.preparing_download_dialog) + .setCancelable(false) + .create() + downloadPreparingDialog.show() + } + + private fun createCancelDialog() { + val builder = MaterialAlertDialogBuilder(requireContext()) + val dialog = builder + .setTitle(CoreR.string.cancel_download) + .setMessage(CoreR.string.cancel_download_message) + .setPositiveButton(CoreR.string.stop_download) { _, _ -> + viewModel.cancelDownload() + } + .setNegativeButton(CoreR.string.cancel) { _, _ -> + } + .create() + dialog.show() + } + private fun navigateToPlayerActivity( playerItems: Array, ) { @@ -266,13 +385,19 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { ) } - private fun navigateToSeries(id: UUID, name: String?) { + private fun navigateToSeries(id: UUID, name: String) { findNavController().navigate( - EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToMediaInfoFragment( + EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToShowFragment( itemId = id, - itemName = name, - itemType = BaseItemKind.SERIES + itemName = name ) ) } + + private fun formatDateTime(datetime: DateTime?): String { + if (datetime == null) return "" + val instant = datetime.toInstant(ZoneOffset.UTC) + val date = Date.from(instant) + return DateFormat.getDateInstance(DateFormat.SHORT).format(date) + } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt index d598eb6f..527b9727 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt @@ -17,10 +17,13 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber @AndroidEntryPoint @@ -40,10 +43,10 @@ class FavoriteFragment : Fragment() { binding.favoritesRecyclerView.adapter = FavoritesListAdapter( ViewItemListAdapter.OnClickListener { item -> - navigateToMediaInfoFragment(item) + navigateToMediaItem(item) }, HomeEpisodeListAdapter.OnClickListener { item -> - navigateToEpisodeBottomSheetFragment(item) + navigateToMediaItem(item) } ) @@ -96,21 +99,31 @@ class FavoriteFragment : Fragment() { checkIfLoginRequired(uiState.error.message) } - private fun navigateToMediaInfoFragment(item: BaseItemDto) { - findNavController().navigate( - FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment( - item.id, - item.name, - item.type - ) - ) - } - - private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { - findNavController().navigate( - FavoriteFragmentDirections.actionFavoriteFragmentToEpisodeBottomSheetFragment( - episode.id - ) - ) + private fun navigateToMediaItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + FavoriteFragmentDirections.actionFavoriteFragmentToMovieFragment( + item.id, + item.name + ) + ) + } + is FindroidShow -> { + findNavController().navigate( + FavoriteFragmentDirections.actionFavoriteFragmentToShowFragment( + item.id, + item.name + ) + ) + } + is FindroidEpisode -> { + findNavController().navigate( + FavoriteFragmentDirections.actionFavoriteFragmentToEpisodeBottomSheetFragment( + item.id + ) + ) + } + } } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt index d39d7d15..4112b484 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt @@ -8,8 +8,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.WindowManager -import android.widget.Toast -import android.widget.Toast.LENGTH_LONG import androidx.appcompat.widget.SearchView import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -21,17 +19,22 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewListAdapter import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.FragmentHomeBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.utils.checkIfLoginRequired +import dev.jdtech.jellyfin.utils.restart import dev.jdtech.jellyfin.viewmodels.HomeViewModel +import javax.inject.Inject import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind import timber.log.Timber @AndroidEntryPoint @@ -44,6 +47,9 @@ class HomeFragment : Fragment() { private lateinit var errorDialog: ErrorDialogFragment + @Inject + lateinit var appPreferences: AppPreferences + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -65,7 +71,6 @@ class HomeFragment : Fragment() { object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(CoreR.menu.home_menu, menu) - val settings = menu.findItem(CoreR.id.action_settings) val search = menu.findItem(CoreR.id.action_search) val searchView = search.actionView as SearchView @@ -142,15 +147,14 @@ class HomeFragment : Fragment() { binding.viewsRecyclerView.adapter = ViewListAdapter( onClickListener = ViewListAdapter.OnClickListener { navigateToLibraryFragment(it) }, onItemClickListener = ViewItemListAdapter.OnClickListener { - navigateToMediaInfoFragment(it) + navigateToMediaItem(it) }, onNextUpClickListener = HomeEpisodeListAdapter.OnClickListener { item -> - when (item.type) { - BaseItemKind.EPISODE -> navigateToEpisodeBottomSheetFragment(item) - BaseItemKind.MOVIE -> navigateToMediaInfoFragment(item) - else -> Toast.makeText(requireContext(), CoreR.string.unknown_error, LENGTH_LONG) - .show() - } + navigateToMediaItem(item) + }, + onOnlineClickListener = ViewListAdapter.OnClickListenerOfflineCard { + appPreferences.offlineMode = false + activity?.restart() } ) @@ -212,34 +216,34 @@ class HomeFragment : Fragment() { ) } - private fun navigateToMediaInfoFragment(item: BaseItemDto) { - if (item.type == BaseItemKind.EPISODE) { - findNavController().navigate( - HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment( - item.seriesId!!, - item.seriesName, - BaseItemKind.SERIES + private fun navigateToMediaItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + HomeFragmentDirections.actionNavigationHomeToMovieFragment( + item.id, + item.name + ) ) - ) - } else { - findNavController().navigate( - HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment( - item.id, - item.name, - item.type + } + is FindroidShow -> { + findNavController().navigate( + HomeFragmentDirections.actionNavigationHomeToShowFragment( + item.id, + item.name + ) ) - ) + } + is FindroidEpisode -> { + findNavController().navigate( + HomeFragmentDirections.actionNavigationHomeToEpisodeBottomSheetFragment( + item.id + ) + ) + } } } - private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { - findNavController().navigate( - HomeFragmentDirections.actionNavigationHomeToEpisodeBottomSheetFragment( - episode.id - ) - ) - } - private fun navigateToSettingsFragment() { findNavController().navigate( HomeFragmentDirections.actionHomeFragmentToSettingsFragment() diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt index 086baac8..eef0f054 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt @@ -26,13 +26,16 @@ import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.SortDialogFragment import dev.jdtech.jellyfin.models.CollectionType +import dev.jdtech.jellyfin.models.FindroidCollection +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.SortBy 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 org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.SortOrder @AndroidEntryPoint @@ -114,9 +117,9 @@ class LibraryFragment : Fragment() { ViewItemPagingAdapter( ViewItemPagingAdapter.OnClickListener { item -> if (args.libraryType == CollectionType.BoxSets.type) { - navigateToCollectionFragment(item) + navigateToItem(item) } else { - navigateToMediaInfoFragment(item) + navigateToItem(item) } } ) @@ -197,22 +200,32 @@ class LibraryFragment : Fragment() { checkIfLoginRequired(uiState.error.message) } - private fun navigateToMediaInfoFragment(item: BaseItemDto) { - findNavController().navigate( - LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment( - item.id, - item.name, - item.type - ) - ) - } - - private fun navigateToCollectionFragment(collection: BaseItemDto) { - findNavController().navigate( - LibraryFragmentDirections.actionLibraryFragmentToCollectionFragment( - collection.id, - collection.name - ) - ) + private fun navigateToItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + LibraryFragmentDirections.actionLibraryFragmentToMovieFragment( + item.id, + item.name + ) + ) + } + is FindroidShow -> { + findNavController().navigate( + LibraryFragmentDirections.actionLibraryFragmentToShowFragment( + item.id, + item.name + ) + ) + } + is FindroidCollection -> { + findNavController().navigate( + LibraryFragmentDirections.actionLibraryFragmentToCollectionFragment( + item.id, + item.name + ) + ) + } + } } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt index 55f5d39c..c47699b1 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt @@ -23,10 +23,10 @@ import dev.jdtech.jellyfin.adapters.CollectionListAdapter import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.FragmentMediaBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidCollection import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.MediaViewModel import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber @AndroidEntryPoint @@ -146,12 +146,12 @@ class MediaFragment : Fragment() { checkIfLoginRequired(uiState.error.message) } - private fun navigateToLibraryFragment(library: BaseItemDto) { + private fun navigateToLibraryFragment(library: FindroidCollection) { findNavController().navigate( MediaFragmentDirections.actionNavigationMediaToLibraryFragment( library.id, library.name, - library.collectionType, + library.type.type, ) ) } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt deleted file mode 100644 index deeb145c..00000000 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ /dev/null @@ -1,451 +0,0 @@ -package dev.jdtech.jellyfin.fragments - -import android.content.Intent -import android.content.res.ColorStateList -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.google.android.material.R as MaterialR -import dagger.hilt.android.AndroidEntryPoint -import dev.jdtech.jellyfin.AppPreferences -import dev.jdtech.jellyfin.adapters.PersonListAdapter -import dev.jdtech.jellyfin.adapters.ViewItemListAdapter -import dev.jdtech.jellyfin.bindBaseItemImage -import dev.jdtech.jellyfin.bindItemBackdropImage -import dev.jdtech.jellyfin.core.R as CoreR -import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding -import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment -import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment -import dev.jdtech.jellyfin.models.AudioCodec -import dev.jdtech.jellyfin.models.DisplayProfile -import dev.jdtech.jellyfin.models.PlayerItem -import dev.jdtech.jellyfin.utils.checkIfLoginRequired -import dev.jdtech.jellyfin.utils.setTintColor -import dev.jdtech.jellyfin.utils.setTintColorAttribute -import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel -import dev.jdtech.jellyfin.viewmodels.PlayerViewModel -import java.util.UUID -import javax.inject.Inject -import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import timber.log.Timber - -@AndroidEntryPoint -class MediaInfoFragment : Fragment() { - - private lateinit var binding: FragmentMediaInfoBinding - private val viewModel: MediaInfoViewModel by viewModels() - private val playerViewModel: PlayerViewModel by viewModels() - private val args: MediaInfoFragmentArgs by navArgs() - - lateinit var errorDialog: ErrorDialogFragment - - @Inject - lateinit var appPreferences: AppPreferences - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentMediaInfoBinding.inflate(inflater, container, false) - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { uiState -> - Timber.d("$uiState") - when (uiState) { - is MediaInfoViewModel.UiState.Normal -> bindUiStateNormal(uiState) - is MediaInfoViewModel.UiState.Loading -> bindUiStateLoading() - is MediaInfoViewModel.UiState.Error -> bindUiStateError(uiState) - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - if (!args.isOffline) { - viewModel.loadData(args.itemId, args.itemType) - } else { - viewModel.loadData(args.playerItem!!) - } - } - } - - if (args.itemType != BaseItemKind.MOVIE) { - binding.downloadButton.visibility = View.GONE - } - - binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadData(args.itemId, args.itemType) - } - - playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems -> - when (playerItems) { - is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems) - is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems) - } - } - - binding.trailerButton.setOnClickListener { - if (viewModel.item?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener - val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse(viewModel.item?.remoteTrailers?.get(0)?.url) - ) - startActivity(intent) - } - - binding.nextUp.setOnClickListener { - navigateToEpisodeBottomSheetFragment(viewModel.nextUp!!) - } - - binding.seasonsRecyclerView.adapter = - ViewItemListAdapter( - ViewItemListAdapter.OnClickListener { season -> - navigateToSeasonFragment(season) - }, - fixedWidth = true - ) - binding.peopleRecyclerView.adapter = PersonListAdapter { person -> - navigateToPersonDetail(person.id) - } - - binding.playButton.setOnClickListener { - binding.playButton.setImageResource(android.R.color.transparent) - binding.progressCircular.isVisible = true - if (viewModel.canRetry) { - binding.playButton.isEnabled = false - viewModel.download() - return@setOnClickListener - } - viewModel.item?.let { item -> - if (!args.isOffline) { - playerViewModel.loadPlayerItems(item) { - VideoVersionDialogFragment(item, playerViewModel).show( - parentFragmentManager, - "videoversiondialog" - ) - } - } else { - playerViewModel.loadOfflinePlayerItems(args.playerItem!!) - } - } - } - - if (!args.isOffline) { - binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadData(args.itemId, args.itemType) - } - - binding.errorLayout.errorDetailsButton.setOnClickListener { - errorDialog.show(parentFragmentManager, ErrorDialogFragment.TAG) - } - - binding.checkButton.setOnClickListener { - when (viewModel.played) { - true -> { - viewModel.markAsUnplayed(args.itemId) - binding.checkButton.setTintColorAttribute( - MaterialR.attr.colorOnSecondaryContainer, - requireActivity().theme - ) - } - - false -> { - viewModel.markAsPlayed(args.itemId) - binding.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) - } - } - } - - binding.favoriteButton.setOnClickListener { - when (viewModel.favorite) { - true -> { - viewModel.unmarkAsFavorite(args.itemId) - binding.favoriteButton.setImageResource(CoreR.drawable.ic_heart) - binding.favoriteButton.setTintColorAttribute( - MaterialR.attr.colorOnSecondaryContainer, - requireActivity().theme - ) - } - - false -> { - viewModel.markAsFavorite(args.itemId) - binding.favoriteButton.setImageResource(CoreR.drawable.ic_heart_filled) - binding.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) - } - } - } - - binding.downloadButton.setOnClickListener { - binding.downloadButton.isEnabled = false - viewModel.download() - binding.downloadButton.imageTintList = ColorStateList.valueOf( - resources.getColor( - CoreR.color.red, - requireActivity().theme - ) - ) - } - } else { - binding.favoriteButton.isVisible = false - binding.checkButton.isVisible = false - binding.downloadButton.isVisible = false - binding.deleteButton.isVisible = true - - binding.deleteButton.setOnClickListener { - viewModel.deleteItem() - findNavController().navigate(CoreR.id.downloadFragment) - } - } - } - - private fun bindUiStateNormal(uiState: MediaInfoViewModel.UiState.Normal) { - uiState.apply { - binding.originalTitle.isVisible = item.originalTitle != item.name - if (item.remoteTrailers.isNullOrEmpty()) { - binding.trailerButton.isVisible = false - } - binding.communityRating.isVisible = item.communityRating != null - binding.actors.isVisible = actors.isNotEmpty() - - val clickable = canPlay && (available || canRetry) - binding.playButton.isEnabled = clickable - binding.playButton.alpha = if (!clickable) 0.5F else 1.0F - binding.playButton.setImageResource(if (!canRetry) CoreR.drawable.ic_play else CoreR.drawable.ic_rotate_ccw) - if (!(available || canRetry)) { - binding.playButton.setImageResource(android.R.color.transparent) - binding.progressCircular.isVisible = true - } - - // Check icon - when (played) { - true -> binding.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) - false -> binding.checkButton.setTintColorAttribute( - MaterialR.attr.colorOnSecondaryContainer, - requireActivity().theme - ) - } - - // Favorite icon - val favoriteDrawable = when (favorite) { - true -> CoreR.drawable.ic_heart_filled - false -> CoreR.drawable.ic_heart - } - binding.favoriteButton.setImageResource(favoriteDrawable) - if (favorite) binding.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) - - when (canDownload) { - true -> { - binding.downloadButton.isVisible = true - binding.downloadButton.isEnabled = !downloaded - - if (downloaded) binding.downloadButton.setTintColor( - CoreR.color.red, - requireActivity().theme - ) - } - - false -> { - binding.downloadButton.isVisible = false - } - } - - binding.name.text = item.name - binding.originalTitle.text = item.originalTitle - if (dateString.isEmpty()) { - binding.year.isVisible = false - } else { - binding.year.text = dateString - } - if (runTime.isEmpty()) { - binding.playtime.isVisible = false - } else { - binding.playtime.text = runTime - } - binding.officialRating.text = item.officialRating - binding.communityRating.text = item.communityRating.toString() - binding.genresLayout.isVisible = item.genres?.isNotEmpty() ?: false - binding.genres.text = genresString - binding.videoMeta.text = videoString - binding.audio.text = audioString - binding.subtitles.text = subtitleString - binding.subsChip.isVisible = subtitleString.isNotEmpty() - - if (appPreferences.displayExtraInfo) { - binding.subtitlesLayout.isVisible = subtitleString.isNotEmpty() - binding.videoMetaLayout.isVisible = videoString.isNotEmpty() - binding.audioLayout.isVisible = audioString.isNotEmpty() - } - - videoMetadata?.let { - with(binding) { - videoMetaChips.isVisible = true - audioChannelChip.text = it.audioChannels.firstOrNull()?.raw - resChip.text = it.resolution.firstOrNull()?.raw - audioChannelChip.isVisible = it.audioChannels.isNotEmpty() - resChip.isVisible = it.resolution.isNotEmpty() - - it.displayProfiles.firstOrNull()?.apply { - videoProfileChip.text = this.raw - videoProfileChip.isVisible = when (this) { - DisplayProfile.HDR, - DisplayProfile.HDR10, - DisplayProfile.HLG -> { - videoProfileChip.chipStartPadding = .0f - true - } - - DisplayProfile.DOLBY_VISION -> { - videoProfileChip.isChipIconVisible = true - true - } - - else -> false - } - } - - audioCodecChip.text = when (val codec = it.audioCodecs.firstOrNull()) { - AudioCodec.AC3, AudioCodec.EAC3, AudioCodec.TRUEHD -> { - audioCodecChip.isVisible = true - if (it.isAtmos.firstOrNull() == true) { - "${codec.raw} | Atmos" - } else codec.raw - } - - AudioCodec.DTS -> { - audioCodecChip.apply { - isVisible = true - isChipIconVisible = false - chipStartPadding = .0f - } - codec.raw - } - - else -> { - audioCodecChip.isVisible = false - null - } - } - } - } - binding.directorLayout.isVisible = director != null - binding.director.text = director?.name - binding.writersLayout.isVisible = writers.isNotEmpty() - binding.writers.text = writersString - binding.description.text = item.overview - binding.nextUpLayout.isVisible = nextUp != null - binding.nextUpName.text = getString( - CoreR.string.episode_name_extended, - nextUp?.parentIndexNumber, - nextUp?.indexNumber, - nextUp?.name - ) - binding.seasonsLayout.isVisible = seasons.isNotEmpty() - val seasonsAdapter = binding.seasonsRecyclerView.adapter as ViewItemListAdapter - seasonsAdapter.submitList(seasons) - val actorsAdapter = binding.peopleRecyclerView.adapter as PersonListAdapter - actorsAdapter.submitList(actors) - bindItemBackdropImage(binding.itemBanner, item) - bindBaseItemImage(binding.nextUpImage, nextUp) - } - binding.loadingIndicator.isVisible = false - binding.mediaInfoScrollview.isVisible = true - binding.errorLayout.errorPanel.isVisible = false - } - - private fun bindUiStateLoading() { - binding.loadingIndicator.isVisible = true - binding.errorLayout.errorPanel.isVisible = false - } - - private fun bindUiStateError(uiState: MediaInfoViewModel.UiState.Error) { - errorDialog = ErrorDialogFragment.newInstance(uiState.error) - binding.loadingIndicator.isVisible = false - binding.mediaInfoScrollview.isVisible = false - binding.errorLayout.errorPanel.isVisible = true - checkIfLoginRequired(uiState.error.message) - } - - private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) { - navigateToPlayerActivity(items.items.toTypedArray()) - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - CoreR.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE - } - - private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) { - Timber.e(error.error.message) - binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - CoreR.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE - binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment.newInstance(error.error) - .show(parentFragmentManager, ErrorDialogFragment.TAG) - } - } - - private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { - findNavController().navigate( - MediaInfoFragmentDirections.actionMediaInfoFragmentToEpisodeBottomSheetFragment( - episode.id - ) - ) - } - - private fun navigateToSeasonFragment(season: BaseItemDto) { - findNavController().navigate( - MediaInfoFragmentDirections.actionMediaInfoFragmentToSeasonFragment( - season.seriesId!!, - season.id, - season.seriesName, - season.name - ) - ) - } - - private fun navigateToPlayerActivity( - playerItems: Array, - ) { - findNavController().navigate( - MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity( - playerItems - ) - ) - } - - private fun navigateToPersonDetail(personId: UUID) { - findNavController().navigate( - MediaInfoFragmentDirections.actionMediaInfoFragmentToPersonDetailFragment(personId) - ) - } -} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt new file mode 100644 index 00000000..b55dd78c --- /dev/null +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt @@ -0,0 +1,511 @@ +package dev.jdtech.jellyfin.fragments + +import android.app.DownloadManager +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.google.android.material.R as MaterialR +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.AppPreferences +import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.adapters.PersonListAdapter +import dev.jdtech.jellyfin.bindItemBackdropImage +import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.databinding.FragmentMovieBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.dialogs.getStorageSelectionDialog +import dev.jdtech.jellyfin.dialogs.getVideoVersionDialog +import dev.jdtech.jellyfin.models.AudioCodec +import dev.jdtech.jellyfin.models.DisplayProfile +import dev.jdtech.jellyfin.models.FindroidSourceType +import dev.jdtech.jellyfin.models.PlayerItem +import dev.jdtech.jellyfin.models.UiText +import dev.jdtech.jellyfin.models.isDownloaded +import dev.jdtech.jellyfin.models.isDownloading +import dev.jdtech.jellyfin.utils.checkIfLoginRequired +import dev.jdtech.jellyfin.utils.setTintColor +import dev.jdtech.jellyfin.utils.setTintColorAttribute +import dev.jdtech.jellyfin.viewmodels.MovieViewModel +import dev.jdtech.jellyfin.viewmodels.PlayerViewModel +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint +class MovieFragment : Fragment() { + private lateinit var binding: FragmentMovieBinding + private val viewModel: MovieViewModel by viewModels() + private val playerViewModel: PlayerViewModel by viewModels() + private val args: MovieFragmentArgs by navArgs() + + private lateinit var errorDialog: ErrorDialogFragment + private lateinit var downloadPreparingDialog: AlertDialog + + @Inject + lateinit var appPreferences: AppPreferences + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentMovieBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.uiState.collect { uiState -> + Timber.d("$uiState") + when (uiState) { + is MovieViewModel.UiState.Normal -> bindUiStateNormal(uiState) + is MovieViewModel.UiState.Loading -> bindUiStateLoading() + is MovieViewModel.UiState.Error -> bindUiStateError(uiState) + } + } + } + + launch { + viewModel.downloadStatus.collect { (status, progress) -> + when (status) { + 10 -> { + downloadPreparingDialog.dismiss() + } + DownloadManager.STATUS_PENDING -> { + binding.itemActions.downloadButton.setImageResource(android.R.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + } + DownloadManager.STATUS_RUNNING -> { + binding.itemActions.downloadButton.setImageResource(android.R.color.transparent) + binding.itemActions.progressDownload.isVisible = true + if (progress < 5) { + binding.itemActions.progressDownload.isIndeterminate = true + } else { + binding.itemActions.progressDownload.isIndeterminate = false + binding.itemActions.progressDownload.setProgressCompat(progress, true) + } + } + DownloadManager.STATUS_SUCCESSFUL -> { + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_trash) + binding.itemActions.progressDownload.isVisible = false + } + else -> { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + } + } + } + + launch { + viewModel.downloadError.collect { uiText -> + createErrorDialog(uiText) + } + } + + launch { + viewModel.navigateBack.collect { + if (it) findNavController().navigateUp() + } + } + } + } + + binding.errorLayout.errorRetryButton.setOnClickListener { + viewModel.loadData(args.itemId) + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + errorDialog.show(parentFragmentManager, ErrorDialogFragment.TAG) + } + + playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems -> + when (playerItems) { + is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems) + is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems) + } + } + + binding.itemActions.playButton.setOnClickListener { + binding.itemActions.playButton.isEnabled = false + binding.itemActions.playButton.setImageResource(android.R.color.transparent) + binding.itemActions.progressCircular.isVisible = true + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), viewModel.item, + onItemSelected = { + playerViewModel.loadPlayerItems(viewModel.item, it) + }, + onCancel = { + playButtonNormal() + } + ) + dialog.show() + return@setOnClickListener + } + playerViewModel.loadPlayerItems(viewModel.item) + } + + binding.itemActions.trailerButton.setOnClickListener { + viewModel.item.trailer.let { trailerUri -> + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse(trailerUri) + ) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show() + } + } + } + + binding.itemActions.checkButton.setOnClickListener { + val played = viewModel.togglePlayed() + bindCheckButtonState(played) + } + + binding.itemActions.favoriteButton.setOnClickListener { + val favorite = viewModel.toggleFavorite() + bindFavoriteButtonState(favorite) + } + + binding.itemActions.downloadButton.setOnClickListener { + if (viewModel.item.isDownloaded()) { + viewModel.deleteItem() + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } else if (viewModel.item.isDownloading()) { + createCancelDialog() + } else { + binding.itemActions.downloadButton.setImageResource(android.R.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { + val storageDialog = getStorageSelectionDialog( + requireContext(), + onItemSelected = { storageIndex -> + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex, storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + ) + dialog.show() + return@getStorageSelectionDialog + } + createDownloadPreparingDialog() + viewModel.download(storageIndex = storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + ) + storageDialog.show() + return@setOnClickListener + } + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + ) + dialog.show() + return@setOnClickListener + } + createDownloadPreparingDialog() + viewModel.download() + } + } + + binding.peopleRecyclerView.adapter = PersonListAdapter { person -> + navigateToPersonDetail(person.id) + } + } + + override fun onResume() { + super.onResume() + + viewModel.loadData(args.itemId) + } + + private fun bindUiStateNormal(uiState: MovieViewModel.UiState.Normal) { + uiState.apply { + val canDownload = + item.canDownload && item.sources.any { it.type == FindroidSourceType.REMOTE } + val canDelete = item.sources.any { it.type == FindroidSourceType.LOCAL } + + binding.originalTitle.isVisible = item.originalTitle != item.name + if (item.trailer != null) { + binding.itemActions.trailerButton.isVisible = true + } + binding.communityRating.isVisible = item.communityRating != null + binding.actors.isVisible = actors.isNotEmpty() + + val canPlay = item.canPlay && item.sources.isNotEmpty() + binding.itemActions.playButton.isEnabled = canPlay + binding.itemActions.playButton.alpha = if (!canPlay) 0.5F else 1.0F + + bindCheckButtonState(item.played) + + bindFavoriteButtonState(item.favorite) + + if (item.isDownloaded()) { + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_trash) + } + + when (canDownload || canDelete) { + true -> binding.itemActions.downloadButton.isVisible = true + false -> binding.itemActions.downloadButton.isVisible = false + } + + binding.name.text = item.name + binding.originalTitle.text = item.originalTitle + if (dateString.isEmpty()) { + binding.year.isVisible = false + } else { + binding.year.text = dateString + } + if (runTime.isEmpty()) { + binding.playtime.isVisible = false + } else { + binding.playtime.text = runTime + } + binding.officialRating.text = item.officialRating + binding.communityRating.text = item.communityRating.toString() + binding.genresLayout.isVisible = item.genres.isNotEmpty() + binding.genres.text = genresString + binding.videoMeta.text = videoString + binding.audio.text = audioString + binding.subtitles.text = subtitleString + binding.subsChip.isVisible = subtitleString.isNotEmpty() + + if (appPreferences.displayExtraInfo) { + binding.subtitlesLayout.isVisible = subtitleString.isNotEmpty() + binding.videoMetaLayout.isVisible = videoString.isNotEmpty() + binding.audioLayout.isVisible = audioString.isNotEmpty() + } + + videoMetadata.let { + with(binding) { + videoMetaChips.isVisible = true + audioChannelChip.text = it.audioChannels.firstOrNull()?.raw + resChip.text = it.resolution.firstOrNull()?.raw + audioChannelChip.isVisible = it.audioChannels.isNotEmpty() + resChip.isVisible = it.resolution.isNotEmpty() + + it.displayProfiles.firstOrNull()?.apply { + videoProfileChip.text = this.raw + videoProfileChip.isVisible = when (this) { + DisplayProfile.HDR, + DisplayProfile.HDR10, + DisplayProfile.HLG -> { + videoProfileChip.chipStartPadding = .0f + true + } + + DisplayProfile.DOLBY_VISION -> { + videoProfileChip.isChipIconVisible = true + true + } + + else -> false + } + } + + audioCodecChip.text = when (val codec = it.audioCodecs.firstOrNull()) { + AudioCodec.AC3, AudioCodec.EAC3, AudioCodec.TRUEHD -> { + audioCodecChip.isVisible = true + if (it.isAtmos.firstOrNull() == true) { + "${codec.raw} | Atmos" + } else codec.raw + } + + AudioCodec.DTS -> { + audioCodecChip.apply { + isVisible = true + isChipIconVisible = false + chipStartPadding = .0f + } + codec.raw + } + + else -> { + audioCodecChip.isVisible = false + null + } + } + } + } + binding.directorLayout.isVisible = director != null + binding.director.text = director?.name + binding.writersLayout.isVisible = writers.isNotEmpty() + binding.writers.text = writersString + binding.description.text = item.overview + val actorsAdapter = binding.peopleRecyclerView.adapter as PersonListAdapter + actorsAdapter.submitList(actors) + bindItemBackdropImage(binding.itemBanner, item) + } + binding.loadingIndicator.isVisible = false + binding.mediaInfoScrollview.isVisible = true + binding.errorLayout.errorPanel.isVisible = false + } + + private fun bindUiStateLoading() { + binding.loadingIndicator.isVisible = true + binding.errorLayout.errorPanel.isVisible = false + } + + private fun bindUiStateError(uiState: MovieViewModel.UiState.Error) { + errorDialog = ErrorDialogFragment.newInstance(uiState.error) + binding.loadingIndicator.isVisible = false + binding.mediaInfoScrollview.isVisible = false + binding.errorLayout.errorPanel.isVisible = true + checkIfLoginRequired(uiState.error.message) + } + + private fun bindCheckButtonState(played: Boolean) { + when (played) { + true -> binding.itemActions.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) + false -> binding.itemActions.checkButton.setTintColorAttribute( + MaterialR.attr.colorOnSecondaryContainer, + requireActivity().theme + ) + } + } + + private fun bindFavoriteButtonState(favorite: Boolean) { + val favoriteDrawable = when (favorite) { + true -> CoreR.drawable.ic_heart_filled + false -> CoreR.drawable.ic_heart + } + binding.itemActions.favoriteButton.setImageResource(favoriteDrawable) + when (favorite) { + true -> binding.itemActions.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) + false -> binding.itemActions.favoriteButton.setTintColorAttribute( + MaterialR.attr.colorOnSecondaryContainer, + requireActivity().theme + ) + } + } + + private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) { + navigateToPlayerActivity(items.items.toTypedArray()) + binding.itemActions.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + CoreR.drawable.ic_play + ) + ) + binding.itemActions.progressCircular.visibility = View.INVISIBLE + } + + private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) { + Timber.e(error.error.message) + binding.playerItemsError.visibility = View.VISIBLE + playButtonNormal() + binding.playerItemsErrorDetails.setOnClickListener { + ErrorDialogFragment.newInstance(error.error) + .show(parentFragmentManager, ErrorDialogFragment.TAG) + } + } + + private fun playButtonNormal() { + binding.itemActions.playButton.isEnabled = true + binding.itemActions.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + CoreR.drawable.ic_play + ) + ) + binding.itemActions.progressCircular.visibility = View.INVISIBLE + } + + private fun createErrorDialog(uiText: UiText) { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder + .setTitle(CoreR.string.downloading_error) + .setMessage(uiText.asString(requireContext().resources)) + .setPositiveButton(getString(CoreR.string.close)) { _, _ -> + } + builder.show() + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setImageResource(CoreR.drawable.ic_download) + } + + private fun createDownloadPreparingDialog() { + val builder = MaterialAlertDialogBuilder(requireContext()) + downloadPreparingDialog = builder + .setTitle(CoreR.string.preparing_download) + .setView(R.layout.preparing_download_dialog) + .setCancelable(false) + .create() + downloadPreparingDialog.show() + } + + private fun createCancelDialog() { + val builder = MaterialAlertDialogBuilder(requireContext()) + val dialog = builder + .setTitle(CoreR.string.cancel_download) + .setMessage(CoreR.string.cancel_download_message) + .setPositiveButton(CoreR.string.stop_download) { _, _ -> + viewModel.cancelDownload() + } + .setNegativeButton(CoreR.string.cancel) { _, _ -> + } + .create() + dialog.show() + } + + private fun navigateToPlayerActivity( + playerItems: Array, + ) { + findNavController().navigate( + MovieFragmentDirections.actionMovieFragmentToPlayerActivity( + playerItems + ) + ) + } + + private fun navigateToPersonDetail(personId: UUID) { + findNavController().navigate( + MovieFragmentDirections.actionMovieFragmentToPersonDetailFragment(personId) + ) + } +} diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/PersonDetailFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/PersonDetailFragment.kt index 742d2899..b0c2f00a 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/PersonDetailFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/PersonDetailFragment.kt @@ -20,10 +20,12 @@ import dev.jdtech.jellyfin.bindItemImage import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.databinding.FragmentPersonDetailBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber @AndroidEntryPoint @@ -118,7 +120,7 @@ internal class PersonDetailFragment : Fragment() { private fun adapter() = ViewItemListAdapter( fixedWidth = true, - onClickListener = ViewItemListAdapter.OnClickListener { navigateToMediaInfoFragment(it) } + onClickListener = ViewItemListAdapter.OnClickListener { navigateToMediaItem(it) } ) private fun setupOverviewExpansion() = binding.overview.post { @@ -137,13 +139,24 @@ internal class PersonDetailFragment : Fragment() { } } - private fun navigateToMediaInfoFragment(item: BaseItemDto) { - findNavController().navigate( - PersonDetailFragmentDirections.actionPersonDetailFragmentToMediaInfoFragment( - itemId = item.id, - itemName = item.name, - itemType = item.type - ) - ) + private fun navigateToMediaItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + PersonDetailFragmentDirections.actionPersonDetailFragmentToMovieFragment( + itemId = item.id, + itemName = item.name + ) + ) + } + is FindroidShow -> { + findNavController().navigate( + PersonDetailFragmentDirections.actionPersonDetailFragmentToShowFragment( + itemId = item.id, + itemName = item.name + ) + ) + } + } } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt index 75729357..c8834fa0 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt @@ -18,10 +18,13 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidMovie +import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber @AndroidEntryPoint @@ -42,10 +45,10 @@ class SearchResultFragment : Fragment() { binding.searchResultsRecyclerView.adapter = FavoritesListAdapter( ViewItemListAdapter.OnClickListener { item -> - navigateToMediaInfoFragment(item) + navigateToMediaItem(item) }, HomeEpisodeListAdapter.OnClickListener { item -> - navigateToEpisodeBottomSheetFragment(item) + navigateToMediaItem(item) } ) @@ -104,21 +107,31 @@ class SearchResultFragment : Fragment() { checkIfLoginRequired(uiState.error.message) } - private fun navigateToMediaInfoFragment(item: BaseItemDto) { - findNavController().navigate( - FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment( - item.id, - item.name, - item.type - ) - ) - } - - private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { - findNavController().navigate( - FavoriteFragmentDirections.actionFavoriteFragmentToEpisodeBottomSheetFragment( - episode.id - ) - ) + private fun navigateToMediaItem(item: FindroidItem) { + when (item) { + is FindroidMovie -> { + findNavController().navigate( + SearchResultFragmentDirections.actionSearchResultFragmentToMovieFragment( + item.id, + item.name + ) + ) + } + is FindroidShow -> { + findNavController().navigate( + SearchResultFragmentDirections.actionSearchResultFragmentToShowFragment( + item.id, + item.name + ) + ) + } + is FindroidEpisode -> { + findNavController().navigate( + SearchResultFragmentDirections.actionSearchResultFragmentToEpisodeBottomSheetFragment( + item.id + ) + ) + } + } } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt index d10dfccb..d944d400 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt @@ -16,10 +16,12 @@ import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.adapters.EpisodeListAdapter import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidEpisode +import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.utils.checkIfLoginRequired +import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import dev.jdtech.jellyfin.viewmodels.SeasonViewModel import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber @AndroidEntryPoint @@ -27,6 +29,7 @@ class SeasonFragment : Fragment() { private lateinit var binding: FragmentSeasonBinding private val viewModel: SeasonViewModel by viewModels() + private val playerViewModel: PlayerViewModel by viewModels() private val args: SeasonFragmentArgs by navArgs() private lateinit var errorDialog: ErrorDialogFragment @@ -45,25 +48,36 @@ class SeasonFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { uiState -> - Timber.d("$uiState") - when (uiState) { - is SeasonViewModel.UiState.Normal -> bindUiStateNormal(uiState) - is SeasonViewModel.UiState.Loading -> bindUiStateLoading() - is SeasonViewModel.UiState.Error -> bindUiStateError(uiState) + launch { + viewModel.uiState.collect { uiState -> + Timber.d("$uiState") + when (uiState) { + is SeasonViewModel.UiState.Normal -> bindUiStateNormal(uiState) + is SeasonViewModel.UiState.Loading -> bindUiStateLoading() + is SeasonViewModel.UiState.Error -> bindUiStateError(uiState) + } + } + } + + launch { + viewModel.navigateBack.collect { + if (it) findNavController().navigateUp() } } } } - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.loadEpisodes(args.seriesId, args.seasonId) - } + binding.errorLayout.errorRetryButton.setOnClickListener { + viewModel.loadEpisodes(args.seriesId, args.seasonId, args.offline) } - binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadEpisodes(args.seriesId, args.seasonId) + playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems -> + when (playerItems) { + is PlayerViewModel.PlayerItems -> { + navigateToPlayerActivity(playerItems.items.toTypedArray()) + } + is PlayerViewModel.PlayerItemError -> {} + } } binding.errorLayout.errorDetailsButton.setOnClickListener { @@ -75,10 +89,15 @@ class SeasonFragment : Fragment() { EpisodeListAdapter.OnClickListener { episode -> navigateToEpisodeBottomSheetFragment(episode) }, - args.seriesId, args.seriesName, args.seasonId, args.seasonName ) } + override fun onResume() { + super.onResume() + + viewModel.loadEpisodes(args.seriesId, args.seasonId, args.offline) + } + private fun bindUiStateNormal(uiState: SeasonViewModel.UiState.Normal) { uiState.apply { val adapter = binding.episodesRecyclerView.adapter as EpisodeListAdapter @@ -102,11 +121,21 @@ class SeasonFragment : Fragment() { checkIfLoginRequired(uiState.error.message) } - private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { + private fun navigateToEpisodeBottomSheetFragment(episode: FindroidEpisode) { findNavController().navigate( SeasonFragmentDirections.actionSeasonFragmentToEpisodeBottomSheetFragment( episode.id ) ) } + + private fun navigateToPlayerActivity( + playerItems: Array, + ) { + findNavController().navigate( + SeasonFragmentDirections.actionSeasonFragmentToPlayerActivity( + playerItems + ) + ) + } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt index 954ea187..01e1284d 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt @@ -9,6 +9,7 @@ import androidx.preference.PreferenceFragmentCompat import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.utils.restart import javax.inject.Inject @AndroidEntryPoint @@ -36,6 +37,11 @@ class SettingsFragment : PreferenceFragmentCompat() { true } + findPreference("pref_offline_mode")?.setOnPreferenceClickListener { + activity?.restart() + true + } + findPreference("privacyPolicy")?.setOnPreferenceClickListener { val intent = Intent( Intent.ACTION_VIEW, diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/ShowFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/ShowFragment.kt new file mode 100644 index 00000000..79ad96b4 --- /dev/null +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/ShowFragment.kt @@ -0,0 +1,348 @@ +package dev.jdtech.jellyfin.fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.google.android.material.R as MaterialR +import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.AppPreferences +import dev.jdtech.jellyfin.adapters.PersonListAdapter +import dev.jdtech.jellyfin.adapters.ViewItemListAdapter +import dev.jdtech.jellyfin.bindCardItemImage +import dev.jdtech.jellyfin.bindItemBackdropImage +import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.databinding.FragmentShowBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.models.FindroidItem +import dev.jdtech.jellyfin.models.FindroidSeason +import dev.jdtech.jellyfin.models.FindroidSourceType +import dev.jdtech.jellyfin.models.PlayerItem +import dev.jdtech.jellyfin.models.isDownloaded +import dev.jdtech.jellyfin.utils.checkIfLoginRequired +import dev.jdtech.jellyfin.utils.setTintColor +import dev.jdtech.jellyfin.utils.setTintColorAttribute +import dev.jdtech.jellyfin.viewmodels.PlayerViewModel +import dev.jdtech.jellyfin.viewmodels.ShowViewModel +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint +class ShowFragment : Fragment() { + + private lateinit var binding: FragmentShowBinding + private val viewModel: ShowViewModel by viewModels() + private val playerViewModel: PlayerViewModel by viewModels() + private val args: ShowFragmentArgs by navArgs() + + private lateinit var errorDialog: ErrorDialogFragment + + @Inject + lateinit var appPreferences: AppPreferences + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentShowBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.uiState.collect { uiState -> + Timber.d("$uiState") + when (uiState) { + is ShowViewModel.UiState.Normal -> bindUiStateNormal(uiState) + is ShowViewModel.UiState.Loading -> bindUiStateLoading() + is ShowViewModel.UiState.Error -> bindUiStateError(uiState) + } + } + } + + launch { + viewModel.navigateBack.collect { + if (it) findNavController().navigateUp() + } + } + } + } + + // TODO make download button work for shows + binding.itemActions.downloadButton.visibility = View.GONE + + binding.errorLayout.errorRetryButton.setOnClickListener { + viewModel.loadData(args.itemId, args.offline) + } + + playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems -> + when (playerItems) { + is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems) + is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems) + } + } + + binding.itemActions.trailerButton.setOnClickListener { + viewModel.item.trailer.let { trailerUri -> + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse(trailerUri) + ) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show() + } + } + } + + binding.nextUp.setOnClickListener { + navigateToEpisodeBottomSheetFragment(viewModel.nextUp!!) + } + + binding.seasonsRecyclerView.adapter = + ViewItemListAdapter( + ViewItemListAdapter.OnClickListener { season -> + if (season is FindroidSeason) navigateToSeasonFragment(season) + }, + fixedWidth = true + ) + binding.peopleRecyclerView.adapter = PersonListAdapter { person -> + navigateToPersonDetail(person.id) + } + + binding.itemActions.playButton.setOnClickListener { + binding.itemActions.playButton.setImageResource(android.R.color.transparent) + binding.itemActions.progressCircular.isVisible = true + playerViewModel.loadPlayerItems(viewModel.item) + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + errorDialog.show(parentFragmentManager, ErrorDialogFragment.TAG) + } + + binding.itemActions.checkButton.setOnClickListener { + val played = viewModel.togglePlayed() + bindCheckButtonState(played) + } + + binding.itemActions.favoriteButton.setOnClickListener { + val favorite = viewModel.toggleFavorite() + bindFavoriteButtonState(favorite) + } + } + + override fun onResume() { + super.onResume() + + viewModel.loadData(args.itemId, args.offline) + } + + private fun bindUiStateNormal(uiState: ShowViewModel.UiState.Normal) { + uiState.apply { + val downloaded = item.isDownloaded() + val canDownload = item.canDownload && item.sources.any { it.type == FindroidSourceType.REMOTE } + + binding.originalTitle.isVisible = item.originalTitle != item.name + if (item.trailer != null) { + binding.itemActions.trailerButton.isVisible = true + } + binding.communityRating.isVisible = item.communityRating != null + binding.actors.isVisible = actors.isNotEmpty() + + val canPlay = item.canPlay /*&& item.sources.isNotEmpty()*/ // TODO currently the sources of a show is always empty, we need a way to check if sources are available + binding.itemActions.playButton.isEnabled = canPlay + binding.itemActions.playButton.alpha = if (!canPlay) 0.5F else 1.0F + + bindCheckButtonState(item.played) + + bindFavoriteButtonState(item.favorite) + + when (canDownload) { + true -> { + binding.itemActions.downloadButton.isVisible = true + binding.itemActions.downloadButton.isEnabled = !downloaded + + if (downloaded) binding.itemActions.downloadButton.setTintColor( + CoreR.color.red, + requireActivity().theme + ) + } + + false -> { + binding.itemActions.downloadButton.isVisible = false + } + } + + binding.name.text = item.name + binding.originalTitle.text = item.originalTitle + if (dateString.isEmpty()) { + binding.year.isVisible = false + } else { + binding.year.text = dateString + } + if (runTime.isEmpty()) { + binding.playtime.isVisible = false + } else { + binding.playtime.text = runTime + } + binding.officialRating.text = item.officialRating + binding.communityRating.text = item.communityRating.toString() + binding.genresLayout.isVisible = item.genres.isNotEmpty() + binding.genres.text = genresString + binding.videoMeta.text = videoString + binding.audio.text = audioString + binding.subtitles.text = subtitleString + + if (appPreferences.displayExtraInfo) { + binding.subtitlesLayout.isVisible = subtitleString.isNotEmpty() + binding.videoMetaLayout.isVisible = videoString.isNotEmpty() + binding.audioLayout.isVisible = audioString.isNotEmpty() + } + + binding.directorLayout.isVisible = director != null + binding.director.text = director?.name + binding.writersLayout.isVisible = writers.isNotEmpty() + binding.writers.text = writersString + binding.description.text = item.overview + binding.nextUpLayout.isVisible = nextUp != null + binding.nextUpName.text = getString( + CoreR.string.episode_name_extended, + nextUp?.parentIndexNumber, + nextUp?.indexNumber, + nextUp?.name + ) + binding.seasonsLayout.isVisible = seasons.isNotEmpty() + val seasonsAdapter = binding.seasonsRecyclerView.adapter as ViewItemListAdapter + seasonsAdapter.submitList(seasons) + val actorsAdapter = binding.peopleRecyclerView.adapter as PersonListAdapter + actorsAdapter.submitList(actors) + bindItemBackdropImage(binding.itemBanner, item) + if (nextUp != null) bindCardItemImage(binding.nextUpImage, nextUp!!) + } + binding.loadingIndicator.isVisible = false + binding.mediaInfoScrollview.isVisible = true + binding.errorLayout.errorPanel.isVisible = false + } + + private fun bindUiStateLoading() { + binding.loadingIndicator.isVisible = true + binding.errorLayout.errorPanel.isVisible = false + } + + private fun bindUiStateError(uiState: ShowViewModel.UiState.Error) { + errorDialog = ErrorDialogFragment.newInstance(uiState.error) + binding.loadingIndicator.isVisible = false + binding.mediaInfoScrollview.isVisible = false + binding.errorLayout.errorPanel.isVisible = true + checkIfLoginRequired(uiState.error.message) + } + + private fun bindCheckButtonState(played: Boolean) { + when (played) { + true -> binding.itemActions.checkButton.setTintColor(CoreR.color.red, requireActivity().theme) + false -> binding.itemActions.checkButton.setTintColorAttribute( + MaterialR.attr.colorOnSecondaryContainer, + requireActivity().theme + ) + } + } + + private fun bindFavoriteButtonState(favorite: Boolean) { + val favoriteDrawable = when (favorite) { + true -> CoreR.drawable.ic_heart_filled + false -> CoreR.drawable.ic_heart + } + binding.itemActions.favoriteButton.setImageResource(favoriteDrawable) + when (favorite) { + true -> binding.itemActions.favoriteButton.setTintColor(CoreR.color.red, requireActivity().theme) + false -> binding.itemActions.favoriteButton.setTintColorAttribute( + MaterialR.attr.colorOnSecondaryContainer, + requireActivity().theme + ) + } + } + + private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) { + navigateToPlayerActivity(items.items.toTypedArray()) + binding.itemActions.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + CoreR.drawable.ic_play + ) + ) + binding.itemActions.progressCircular.visibility = View.INVISIBLE + } + + private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) { + Timber.e(error.error.message) + binding.playerItemsError.visibility = View.VISIBLE + binding.itemActions.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + CoreR.drawable.ic_play + ) + ) + binding.itemActions.progressCircular.visibility = View.INVISIBLE + binding.playerItemsErrorDetails.setOnClickListener { + ErrorDialogFragment.newInstance(error.error) + .show(parentFragmentManager, ErrorDialogFragment.TAG) + } + } + + private fun navigateToEpisodeBottomSheetFragment(episode: FindroidItem) { + findNavController().navigate( + ShowFragmentDirections.actionShowFragmentToEpisodeBottomSheetFragment( + episode.id + ) + ) + } + + private fun navigateToSeasonFragment(season: FindroidSeason) { + findNavController().navigate( + ShowFragmentDirections.actionShowFragmentToSeasonFragment( + season.seriesId, + season.id, + season.seriesName, + season.name, + args.offline + ) + ) + } + + private fun navigateToPlayerActivity( + playerItems: Array, + ) { + findNavController().navigate( + ShowFragmentDirections.actionShowFragmentToPlayerActivity( + playerItems + ) + ) + } + + private fun navigateToPersonDetail(personId: UUID) { + findNavController().navigate( + ShowFragmentDirections.actionShowFragmentToPersonDetailFragment(personId) + ) + } +} diff --git a/app/phone/src/main/res/layout-w600dp/fragment_media_info.xml b/app/phone/src/main/res/layout-w600dp/fragment_show.xml similarity index 75% rename from app/phone/src/main/res/layout-w600dp/fragment_media_info.xml rename to app/phone/src/main/res/layout-w600dp/fragment_show.xml index f7291e5c..1a7be92c 100644 --- a/app/phone/src/main/res/layout-w600dp/fragment_media_info.xml +++ b/app/phone/src/main/res/layout-w600dp/fragment_show.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - tools:context=".fragments.MediaInfoFragment"> + tools:context=".fragments.ShowFragment"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginBottom="16dp" /> + type="dev.jdtech.jellyfin.models.FindroidItem" /> - - - + app:layout_constraintTop_toTopOf="@id/item_image"> + + + + + + + + + \ No newline at end of file diff --git a/app/phone/src/main/res/layout/card_offline.xml b/app/phone/src/main/res/layout/card_offline.xml new file mode 100644 index 00000000..1a2975de --- /dev/null +++ b/app/phone/src/main/res/layout/card_offline.xml @@ -0,0 +1,47 @@ + + + + + + + + + +