diff --git a/tvmon-app/app/build.gradle b/tvmon-app/app/build.gradle
index 2a79548..8aaf3a4 100644
--- a/tvmon-app/app/build.gradle
+++ b/tvmon-app/app/build.gradle
@@ -63,6 +63,9 @@ dependencies {
implementation 'androidx.tv:tv-material:1.0.0-alpha10'
implementation 'com.google.android.material:material:1.11.0'
+ // LeakCanary for memory leak detection (TV-optimized with Toast notifications)
+ debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
+
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/TvmonApplication.kt b/tvmon-app/app/src/main/java/com/example/tvmon/TvmonApplication.kt
index 4632c1d..16632c6 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/TvmonApplication.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/TvmonApplication.kt
@@ -1,6 +1,7 @@
package com.example.tvmon
import android.app.Application
+import android.util.Log
import com.example.tvmon.di.appModules
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@@ -8,14 +9,20 @@ import org.koin.core.context.startKoin
import org.koin.core.logger.Level
class TvmonApplication : Application() {
-
+
+ companion object {
+ private const val TAG = "TvmonApplication"
+ }
+
override fun onCreate() {
super.onCreate()
-
+
startKoin {
androidLogger(Level.ERROR)
androidContext(this@TvmonApplication)
modules(appModules)
}
+
+ Log.d(TAG, "TvmonApplication initialized")
}
-}
+}
\ No newline at end of file
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/TvmonGlideModule.kt b/tvmon-app/app/src/main/java/com/example/tvmon/TvmonGlideModule.kt
index a7eca1b..fdacdd5 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/TvmonGlideModule.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/TvmonGlideModule.kt
@@ -3,17 +3,30 @@ package com.example.tvmon
import android.content.Context
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.annotation.GlideModule
+import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.engine.cache.LruResourceCache
import com.bumptech.glide.module.AppGlideModule
+import com.bumptech.glide.request.RequestOptions
@GlideModule
class TvmonGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
- val memoryCacheSizeBytes = 1024 * 1024 * 50
+ // TV-optimized memory cache: 30MB (lower than phone due to limited RAM)
+ val memoryCacheSizeBytes = 1024 * 1024 * 30
builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong()))
-
- val diskCacheSizeBytes = 1024L * 1024L * 200L
+
+ // Disk cache: 150MB
+ val diskCacheSizeBytes = 1024L * 1024L * 150L
builder.setDiskCache(InternalCacheDiskCacheFactory(context, "image_cache", diskCacheSizeBytes))
+
+ // Use RGB_565 for lower memory usage (good quality for TV)
+ builder.setDefaultRequestOptions(
+ RequestOptions()
+ .format(DecodeFormat.PREFER_RGB_565)
+ .disallowHardwareConfig() // Avoid hardware bitmaps that can cause issues on TV
+ )
}
+
+ override fun isManifestParsingEnabled(): Boolean = false
}
\ No newline at end of file
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt
index a66dc48..894a976 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt
@@ -86,7 +86,7 @@ class DetailsFragment : DetailsSupportFragment() {
Log.w(TAG, "setupDetails: Setting up details UI")
val presenterSelector = ClassPresenterSelector()
val detailsPresenter = FullWidthDetailsOverviewRowPresenter(DetailsDescriptionPresenter())
- detailsPresenter.actionsBackgroundColor = ContextCompat.getColor(requireContext(), R.color.primary)
+ detailsPresenter.actionsBackgroundColor = ContextCompat.getColor(requireContext(), R.color.accent)
detailsPresenter.backgroundColor = ContextCompat.getColor(requireContext(), R.color.detail_background)
presenterSelector.addClassPresenter(DetailsOverviewRow::class.java, detailsPresenter)
presenterSelector.addClassPresenter(ListRow::class.java, ListRowPresenter())
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt
index aee7981..5a1191d 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt
@@ -46,26 +46,26 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
}
}
- private fun setupUI() {
+private fun setupUI() {
Log.w(TAG, "setupUI: Setting up UI")
headersState = HEADERS_ENABLED
title = getString(R.string.browse_title)
- brandColor = ContextCompat.getColor(requireContext(), R.color.primary)
- searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.search_opaque)
+ brandColor = ContextCompat.getColor(requireContext(), R.color.accent)
+ searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.accent)
adapter = rowsAdapter
onItemViewClickedListener = this
onItemViewSelectedListener = this
-setOnSearchClickedListener {
- val activity = requireActivity() as? MainActivity
- activity?.showSearchDialog { query ->
- val intent = Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java).apply {
- putExtra("initial_query", query)
+ setOnSearchClickedListener {
+ val activity = requireActivity() as? MainActivity
+ activity?.showSearchDialog { query ->
+ val intent = Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java).apply {
+ putExtra("initial_query", query)
+ }
+ startActivity(intent)
+ }
}
- startActivity(intent)
- }
- }
Log.w(TAG, "setupUI: UI setup complete, adapter set")
}
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt
index b29a394..b41b211 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt
@@ -2,6 +2,8 @@ package com.example.tvmon.ui.presenter
import android.view.View
import android.view.ViewGroup
+import android.view.animation.OvershootInterpolator
+import androidx.core.content.ContextCompat
import androidx.leanback.widget.ImageCardView
import androidx.leanback.widget.Presenter
import com.bumptech.glide.Glide
@@ -11,107 +13,150 @@ import com.example.tvmon.data.model.Category
class ContentCardPresenter : Presenter() {
- override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
- val cardView = ImageCardView(parent.context).apply {
- isFocusable = true
- isFocusableInTouchMode = true
- setBackgroundColor(parent.context.getColor(R.color.default_background))
+ companion object {
+ private const val FOCUS_SCALE = 1.08f
+ private const val FOCUS_TRANSLATION_Z = 8f
+ private const val ANIMATION_DURATION = 150L
}
- return ViewHolder(cardView)
- }
- override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
- val content = item as Content
- val cardView = viewHolder.view as ImageCardView
- val res = cardView.context.resources
-
- cardView.titleText = content.title
- cardView.contentText = null
-
- val width = res.getDimensionPixelSize(R.dimen.card_width)
- val height = res.getDimensionPixelSize(R.dimen.card_height)
- cardView.setMainImageDimensions(width, height)
-
- if (content.thumbnail.isNotBlank()) {
- Glide.with(cardView.context)
- .load(content.thumbnail)
- .error(R.drawable.default_background)
- .placeholder(R.drawable.default_background)
- .centerCrop()
- .into(cardView.mainImageView)
- } else {
- cardView.mainImageView.setImageResource(R.drawable.default_background)
+ override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
+ val cardView = ImageCardView(parent.context).apply {
+ isFocusable = true
+ isFocusableInTouchMode = true
+ setBackgroundColor(ContextCompat.getColor(context, R.color.default_background))
+ setOnFocusChangeListener { view, hasFocus ->
+ animateFocus(view, hasFocus)
+ }
+ }
+ return ViewHolder(cardView)
}
- }
- override fun onUnbindViewHolder(viewHolder: ViewHolder) {
- val cardView = viewHolder.view as ImageCardView
- cardView.badgeImage = null
- cardView.mainImage = null
- }
+ private fun animateFocus(view: View, hasFocus: Boolean) {
+ if (hasFocus) {
+ view.animate()
+ .scaleX(FOCUS_SCALE)
+ .scaleY(FOCUS_SCALE)
+ .translationZ(FOCUS_TRANSLATION_Z)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OvershootInterpolator(1.2f))
+ .start()
+ view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.focused_bg))
+ } else {
+ view.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .translationZ(0f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OvershootInterpolator(1.2f))
+ .start()
+ view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.default_background))
+ }
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
+ val content = item as Content
+ val cardView = viewHolder.view as ImageCardView
+ val res = cardView.context.resources
+
+ cardView.titleText = content.title
+ cardView.contentText = null
+
+ val width = res.getDimensionPixelSize(R.dimen.card_width)
+ val height = res.getDimensionPixelSize(R.dimen.card_height)
+ cardView.setMainImageDimensions(width, height)
+
+ if (content.thumbnail.isNotBlank()) {
+ Glide.with(cardView.context)
+ .load(content.thumbnail)
+ .error(R.drawable.default_background)
+ .placeholder(R.drawable.default_background)
+ .centerCrop()
+ .into(cardView.mainImageView)
+ } else {
+ cardView.mainImageView.setImageResource(R.drawable.default_background)
+ }
+ }
+
+ override fun onUnbindViewHolder(viewHolder: ViewHolder) {
+ val cardView = viewHolder.view as ImageCardView
+ cardView.badgeImage = null
+ cardView.mainImage = null
+ }
}
class SearchCardPresenter : Presenter() {
- override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
- val cardView = ImageCardView(parent.context).apply {
- isFocusable = true
- isFocusableInTouchMode = true
- setBackgroundColor(parent.context.getColor(R.color.default_background))
- cardType = ImageCardView.CARD_TYPE_INFO_UNDER
- infoVisibility = View.VISIBLE
+ companion object {
+ private const val FOCUS_SCALE = 1.1f
+ private const val FOCUS_TRANSLATION_Z = 10f
+ private const val ANIMATION_DURATION = 100L
}
- cardView.setOnFocusChangeListener { v, hasFocus ->
- if (hasFocus) {
- v.animate()
- .scaleX(1.1f)
- .scaleY(1.1f)
- .translationZ(10f)
- .setDuration(100)
- .start()
- } else {
- v.animate()
- .scaleX(1.0f)
- .scaleY(1.0f)
- .translationZ(0f)
- .setDuration(100)
- .start()
- }
+ override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
+ val cardView = ImageCardView(parent.context).apply {
+ isFocusable = true
+ isFocusableInTouchMode = true
+ setBackgroundColor(ContextCompat.getColor(context, R.color.default_background))
+ cardType = ImageCardView.CARD_TYPE_INFO_UNDER
+ infoVisibility = View.VISIBLE
+ setOnFocusChangeListener { view, hasFocus ->
+ animateFocus(view, hasFocus)
+ }
+ }
+ return ViewHolder(cardView)
}
- return ViewHolder(cardView)
- }
-
- override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
- val content = item as Content
- val cardView = viewHolder.view as ImageCardView
- val res = cardView.context.resources
-
- cardView.titleText = content.title
- cardView.contentText = content.category
-
- val width = res.getDimensionPixelSize(R.dimen.card_width)
- val height = res.getDimensionPixelSize(R.dimen.card_height)
- cardView.setMainImageDimensions(width, height)
-
- if (content.thumbnail.isNotBlank()) {
- Glide.with(cardView.context)
- .load(content.thumbnail)
- .error(R.drawable.default_background)
- .placeholder(R.drawable.default_background)
- .centerCrop()
- .into(cardView.mainImageView)
- } else {
- cardView.mainImageView.setImageResource(R.drawable.default_background)
+ private fun animateFocus(view: View, hasFocus: Boolean) {
+ if (hasFocus) {
+ view.animate()
+ .scaleX(FOCUS_SCALE)
+ .scaleY(FOCUS_SCALE)
+ .translationZ(FOCUS_TRANSLATION_Z)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OvershootInterpolator(1.2f))
+ .start()
+ view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.focused_bg))
+ } else {
+ view.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .translationZ(0f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OvershootInterpolator(1.2f))
+ .start()
+ view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.default_background))
+ }
}
- }
- override fun onUnbindViewHolder(viewHolder: ViewHolder) {
- val cardView = viewHolder.view as ImageCardView
- cardView.badgeImage = null
- cardView.mainImage = null
- }
+ override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
+ val content = item as Content
+ val cardView = viewHolder.view as ImageCardView
+ val res = cardView.context.resources
+
+ cardView.titleText = content.title
+ cardView.contentText = content.category
+
+ val width = res.getDimensionPixelSize(R.dimen.card_width)
+ val height = res.getDimensionPixelSize(R.dimen.card_height)
+ cardView.setMainImageDimensions(width, height)
+
+ if (content.thumbnail.isNotBlank()) {
+ Glide.with(cardView.context)
+ .load(content.thumbnail)
+ .error(R.drawable.default_background)
+ .placeholder(R.drawable.default_background)
+ .centerCrop()
+ .into(cardView.mainImageView)
+ } else {
+ cardView.mainImageView.setImageResource(R.drawable.default_background)
+ }
+ }
+
+ override fun onUnbindViewHolder(viewHolder: ViewHolder) {
+ val cardView = viewHolder.view as ImageCardView
+ cardView.badgeImage = null
+ cardView.mainImage = null
+ }
}
class CategoryCardPresenter : Presenter() {
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/EpisodePresenter.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/EpisodePresenter.kt
index 180fe61..3c0d13c 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/EpisodePresenter.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/EpisodePresenter.kt
@@ -1,6 +1,9 @@
package com.example.tvmon.ui.presenter
+import android.view.View
import android.view.ViewGroup
+import android.view.animation.OvershootInterpolator
+import androidx.core.content.ContextCompat
import androidx.leanback.widget.ImageCardView
import androidx.leanback.widget.Presenter
import com.example.tvmon.R
@@ -10,31 +13,62 @@ class EpisodePresenter(
private val watchedEpisodeUrl: String? = null
) : Presenter() {
+ companion object {
+ private const val FOCUS_SCALE = 1.08f
+ private const val FOCUS_TRANSLATION_Z = 8f
+ private const val ANIMATION_DURATION = 150L
+ }
+
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val cardView = ImageCardView(parent.context).apply {
isFocusable = true
isFocusableInTouchMode = true
- setBackgroundColor(parent.context.getColor(R.color.default_background))
+ setBackgroundColor(ContextCompat.getColor(context, R.color.default_background))
+ setOnFocusChangeListener { view, hasFocus ->
+ animateFocus(view, hasFocus)
+ }
}
return ViewHolder(cardView)
}
+ private fun animateFocus(view: View, hasFocus: Boolean) {
+ if (hasFocus) {
+ view.animate()
+ .scaleX(FOCUS_SCALE)
+ .scaleY(FOCUS_SCALE)
+ .translationZ(FOCUS_TRANSLATION_Z)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OvershootInterpolator(1.2f))
+ .start()
+ view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.focused_bg))
+ } else {
+ view.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .translationZ(0f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(OvershootInterpolator(1.2f))
+ .start()
+ view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.default_background))
+ }
+ }
+
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
val episode = item as Episode
val cardView = viewHolder.view as ImageCardView
- val res = cardView.context.resources
+ val ctx = cardView.context
cardView.titleText = episode.number
cardView.contentText = episode.title
if (episode.url == watchedEpisodeUrl) {
- cardView.setBackgroundColor(res.getColor(R.color.accent))
+ cardView.setBackgroundColor(ContextCompat.getColor(ctx, R.color.accent))
} else {
- cardView.setBackgroundColor(res.getColor(R.color.default_background))
+ cardView.setBackgroundColor(ContextCompat.getColor(ctx, R.color.default_background))
}
- val width = res.getDimensionPixelSize(R.dimen.episode_card_width)
- val height = res.getDimensionPixelSize(R.dimen.episode_card_height)
+ val width = ctx.resources.getDimensionPixelSize(R.dimen.episode_card_width)
+ val height = ctx.resources.getDimensionPixelSize(R.dimen.episode_card_height)
cardView.setMainImageDimensions(width, height)
cardView.mainImageView.setImageResource(R.drawable.episode_placeholder)
diff --git a/tvmon-app/app/src/main/res/values/colors.xml b/tvmon-app/app/src/main/res/values/colors.xml
index 60cd507..76ff718 100644
--- a/tvmon-app/app/src/main/res/values/colors.xml
+++ b/tvmon-app/app/src/main/res/values/colors.xml
@@ -1,10 +1,20 @@
- #1F1F1F
+
+ #000000
#000000
- #FF6B6B
- #282828
- #3A3A3A
- #1F1F1F
+ #E50914
+ #E50914
+ #000000
+ #141414
+ #333333
+ #666666
+ #141414
+ #1F1F1F
+ #000000
#AAFFFFFF
+ #FFFFFF
+ #B3000000
+ #333333
+ #2A2A2A
diff --git a/tvmon-app/app/src/main/res/values/dimens.xml b/tvmon-app/app/src/main/res/values/dimens.xml
index 60c9e0c..1b471ac 100644
--- a/tvmon-app/app/src/main/res/values/dimens.xml
+++ b/tvmon-app/app/src/main/res/values/dimens.xml
@@ -1,14 +1,20 @@
- 180dp
- 240dp
-
+
+ 200dp
+ 280dp
+
240dp
160dp
-
- 200dp
- 120dp
-
+
+ 220dp
+ 130dp
+
120dp
160dp
+
+
+ 28dp
+ 12dp
+ 48dp
diff --git a/tvmon-app/app/src/main/res/values/themes.xml b/tvmon-app/app/src/main/res/values/themes.xml
index 8050807..331d03e 100644
--- a/tvmon-app/app/src/main/res/values/themes.xml
+++ b/tvmon-app/app/src/main/res/values/themes.xml
@@ -1,10 +1,11 @@
+