Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;

import org.jellyfin.androidtv.R;
Expand All @@ -20,16 +21,35 @@ public ChannelCardView(Context context) {
}

public void setItem(final BaseItemDto channel) {
if (channel == null) return;
if (channel.getNumber() != null) binding.name.setText(channel.getNumber() + " " + channel.getName());
else binding.name.setText(channel.getName());

boolean isFavorite = channel.getUserData() != null && channel.getUserData().isFavorite();
binding.favImage.setVisibility(isFavorite ? View.VISIBLE : View.GONE);

BaseItemDto program = channel.getCurrentProgram();
if (program != null) {
updateDisplay(program);
updateRecordingIndicator(program);
} else {
binding.program.setText(R.string.no_program_data);
binding.time.setText("");
binding.progress.setProgress(0);
binding.recIndicator.setVisibility(View.GONE);
}
}

private void updateRecordingIndicator(BaseItemDto program) {
if (program.getSeriesTimerId() != null) {
binding.recIndicator.setImageResource(program.getTimerId() != null
? R.drawable.ic_record_series_red : R.drawable.ic_record_series);
binding.recIndicator.setVisibility(View.VISIBLE);
} else if (program.getTimerId() != null) {
binding.recIndicator.setImageResource(R.drawable.ic_record_red);
binding.recIndicator.setVisibility(View.VISIBLE);
} else {
binding.recIndicator.setVisibility(View.GONE);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.LocationType
import org.jellyfin.sdk.model.api.MediaType
Expand Down Expand Up @@ -40,6 +41,7 @@ fun loadLiveTvChannels(fragment: Fragment, callback: (channels: Collection<BaseI
withContext(Dispatchers.IO) {
api.liveTvApi.getLiveTvChannels(
addCurrentProgram = true,
fields = setOf(ItemFields.OVERVIEW),
enableFavoriteSorting = liveTvPreferences[LiveTvPreferences.favsAtTop],
sortBy = if (sortDatePlayed) setOf(ItemSortBy.DATE_PLAYED) else setOf(ItemSortBy.SORT_NAME),
sortOrder = if (sortDatePlayed) SortOrder.DESCENDING else SortOrder.ASCENDING,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.jellyfin.androidtv.ui.presentation

import androidx.leanback.widget.ObjectAdapter

class CircularObjectAdapter(
private val delegate: ObjectAdapter,
) : ObjectAdapter(delegate.presenterSelector) {
companion object {
private const val MULTIPLIER = 1_000
}

val realSize: Int get() = delegate.size()

override fun size(): Int = if (realSize == 0) 0 else realSize * MULTIPLIER

override fun get(position: Int): Any {
val size = realSize
if (size == 0) {
throw IndexOutOfBoundsException("CircularObjectAdapter is empty")
}

return delegate.get(Math.floorMod(position, size))!!
}

fun centerPosition(realIndex: Int): Int {
val size = realSize
if (size == 0 || realIndex < 0 || realIndex >= size) return -1
return (MULTIPLIER / 2) * size + realIndex
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package org.jellyfin.androidtv.ui.presentation

import android.view.KeyEvent
import androidx.leanback.widget.ObjectAdapter
import androidx.leanback.widget.RowPresenter
import timber.log.Timber

class PositionableListRowPresenter : CustomListRowPresenter {
private var viewHolder: ViewHolder? = null
private var pendingPosition: Int = -1
private val trapFocus: Boolean

constructor() : super()
constructor(padding: Int?) : super(padding)
constructor() : this(padding = null, trapFocus = false)
constructor(padding: Int?) : this(padding, trapFocus = false)
constructor(padding: Int? = null, trapFocus: Boolean = false) : super(padding) {
this.trapFocus = trapFocus
}

init {
shadowEnabled = false
Expand All @@ -22,12 +28,64 @@ class PositionableListRowPresenter : CustomListRowPresenter {
if (holder !is ViewHolder) return

viewHolder = holder
val grid = holder.gridView
if (trapFocus) {
// Prevent focus from escaping the grid at either boundary so the user
// stays inside the popup (channel changer / chapter selector).
// Uses the adapter size to detect boundaries rather than hard-coding
// position 0, so this works regardless of the adapter's centering strategy.
grid.setOnKeyInterceptListener { event ->
val adapter = grid.adapter as? ObjectAdapter
val pos = grid.selectedPosition
val size = adapter?.size() ?: 0
when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> pos <= 0
KeyEvent.KEYCODE_DPAD_RIGHT -> size > 0 && pos >= size - 1
else -> false
}
}
}

if (pendingPosition >= 0) {
val pos = pendingPosition
pendingPosition = -1
// Defer until after layout so the grid has items to scroll to.
grid.post {
grid.selectedPosition = pos
}
}
}

override fun onUnbindRowViewHolder(holder: RowPresenter.ViewHolder) {
if (holder === viewHolder) viewHolder = null
super.onUnbindRowViewHolder(holder)
}

/**
* Clear the cached viewHolder so the next [position] set falls through
* to [pendingPosition]. Call this after removing a row from the adapter
* when RecyclerView defers the actual unbind to the next layout pass.
*/
fun invalidate() {
viewHolder = null
}

var position: Int
get() = viewHolder?.gridView?.selectedPosition ?: -1
get() = viewHolder?.gridView?.selectedPosition ?: pendingPosition
set(value) {
Timber.d("Setting position to $value")
viewHolder?.gridView?.selectedPosition = value
val grid = viewHolder?.gridView
if (grid != null && grid.isAttachedToWindow) {
grid.selectedPosition = value
pendingPosition = -1
} else if (grid != null) {
// Grid is bound but not yet attached to the window (e.g. the
// row was just added to the adapter and layout hasn't run).
// Post so the position is applied once the grid is laid out.
pendingPosition = -1
grid.post { grid.selectedPosition = value }
} else {
// Grid not bound yet — store for onBindRowViewHolder.
pendingPosition = value
}
}
}
16 changes: 16 additions & 0 deletions app/src/main/res/drawable/channel_card_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="@color/lb_basic_card_info_bg_color" />
<stroke
android:width="2dp"
android:color="@color/white" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/lb_basic_card_info_bg_color" />
</shape>
</item>
</selector>
28 changes: 26 additions & 2 deletions app/src/main/res/layout/view_card_channel.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,42 @@
android:layout_width="200dp"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/lb_basic_card_info_bg_color"
android:background="@drawable/channel_card_background"
android:duplicateParentState="true"
android:padding="14dp"
android:gravity="center">

<ImageView
android:id="@+id/favImage"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginEnd="4dp"
android:layout_centerVertical="false"
android:src="@drawable/ic_heart_red"
android:visibility="gone" />

<ImageView
android:id="@+id/recIndicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="4dp"
android:scaleType="fitCenter"
android:visibility="gone"
tools:src="@drawable/ic_record_series_red" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="WBTV 3.1"
android:id="@+id/name"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_toEndOf="@+id/favImage"
android:layout_toStartOf="@+id/recIndicator"
android:maxLines="1"
android:ellipsize="end" />

Expand Down
38 changes: 31 additions & 7 deletions app/src/main/res/layout/vlc_player_interface.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,46 @@

</FrameLayout>

<FrameLayout
<LinearLayout
android:id="@+id/popupArea"
android:layout_width="fill_parent"
android:layout_height="225dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/black_transparent"
android:layout_marginBottom="16dp"
android:background="@color/black_opaque"
android:orientation="vertical"
android:visibility="gone">

<TextView
android:id="@+id/popupHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="48dp"
android:paddingTop="8dp"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />

<TextView
android:id="@+id/popupDescription"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingHorizontal="48dp"
android:paddingTop="4dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/white"
android:textSize="16sp"
android:visibility="gone" />

<LinearLayout
android:id="@+id/rows_area"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="start|bottom"
android:layout_marginTop="16dp"
android:layout_height="130dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal" />
</FrameLayout>
</LinearLayout>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topPanel"
Expand Down
Loading