App v0.2.26 and gateway 0.1.20

Client: seek controls, Bazarr subtitle download and cast panel in the
player; MDBList ratings strip; episode and schedule detail pages; series
pace estimate; what's new panel; install-permission onboarding step;
synced per-profile preferences; Emby outage banner.

Gateway: rebuilt admin console (one fragment per page), preference
history and restore, merged Continue Watching, Emby health probe,
subtitle selection and Bazarr download, structured request logging with
per-request identity, and embedded build version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ponzischeme89
2026-08-06 22:33:56 +12:00
co-authored by Claude Opus 5
parent 2675e6d82b
commit 4a4df7a73c
257 changed files with 24868 additions and 3108 deletions
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FF090B0D" android:state_focused="true" />
<item android:color="#FFFFFFFF" />
<item android:color="#FF07090A" android:state_focused="true" />
<item android:color="#FF52B54B" android:state_selected="true" />
<item android:color="#FFE9EDF0" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Two people, for the cast panel. Deliberately not Android's "cast" glyph (the screen
with waves), which on a television means sending the picture somewhere else and is the
one thing this button must not be mistaken for. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5V19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45V19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The mirror of ic_player_rewind, so the pair reads as one control rather than two. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF69CD61"
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z" />
</vector>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Two chevrons pointing back. Deliberately not a clock or a circular arrow: the OSD
already prints the number of seconds, and the glyph only has to say which way. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF69CD61"
android:pathData="M11,18V6l-8.5,6L11,18zM11.5,12l8.5,6V6L11.5,12z" />
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Drawn over the portrait, not behind it, so the accent ring reads on the artwork rather
than being covered by it. A remote has no hover: the ring plus the scale is the only
thing saying which face is selected. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="#00000000" />
<stroke
android:width="2dp"
android:color="#FF52B54B" />
<corners android:radius="12dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#00000000" />
<stroke
android:width="1dp"
android:color="#1FFFFFFF" />
<corners android:radius="12dp" />
</shape>
</item>
</selector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The cast panel has no card edge. It sits on somebody's film, so it fades into the
picture from the bottom rather than dropping a box over it: the scene stays visible
above the names, which is the whole reason the panel was opened. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:centerColor="#D40A0C0F"
android:centerY="0.35"
android:endColor="#00000000"
android:startColor="#FA07080A"
android:type="linear" />
</shape>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The subtitle drop-up sits over somebody's film, so it is near-black rather than a lit
panel: a bright surface at night is the thing that hurts, and the option under focus is
the only lit thing on it. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F7000000" />
<stroke
android:width="1dp"
android:color="#1FFFFFFF" />
<corners android:radius="14dp" />
</shape>
@@ -1,27 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Focus is the only fill on the menu; the current choice is a quiet grey plate, so an
unfocused row never competes with the one the remote is on. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="#FFFFFFFF" />
<corners android:radius="12dp" />
<solid android:color="#FF52B54B" />
<corners android:radius="10dp" />
</shape>
</item>
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="#2E52B54B" />
<stroke
android:width="1dp"
android:color="#B852B54B" />
<corners android:radius="12dp" />
<solid android:color="#1AFFFFFF" />
<corners android:radius="10dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#16FFFFFF" />
<stroke
android:width="1dp"
android:color="#20FFFFFF" />
<corners android:radius="12dp" />
<solid android:color="#00000000" />
<corners android:radius="10dp" />
</shape>
</item>
</selector>
@@ -22,6 +22,11 @@
controller, so it can appear briefly without opening the transport controls. -->
<include layout="@layout/player_playback_identity" />
<!-- Skipping. Declared before the timing cues so those sit above it: a lower third on
the left and this centred chip do not overlap, and if they ever do, the cue that
appears once matters more than one that appears on every press. -->
<include layout="@layout/player_seek_indicator" />
<!-- The ten-minute cue. Declared before the next-up banner so that banner covers it
if the two ever coincide: what comes next matters more than how long is left. -->
<include layout="@layout/player_time_remaining" />
@@ -242,6 +242,15 @@
android:src="@drawable/exo_ic_subtitle_on"
tools:ignore="PrivateResource" />
<!-- Beside the subtitle picker rather than buried in the settings list:
"who is this?" is asked mid-scene, and a question that has to survive a
dialog and four menu rows is one nobody asks twice. -->
<ImageButton
android:id="@+id/player_cast"
style="@style/MembyPlayerControlButton"
android:contentDescription="@string/player_cast_open"
android:src="@drawable/ic_player_cast" />
<ImageButton
android:id="@id/exo_settings"
style="@style/MembyPlayerControlButton"
+35 -18
View File
@@ -1,64 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The cast panel. It covers the bottom of somebody's film, so it is a fade rather than a
card: the scrim carries the names and the scene stays legible above them. The paddings
match the transport row's 48dp side inset, so opening this does not shift the column
the title and controls are read in. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/player_cast_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#70000000"
android:background="@drawable/player_cast_scrim"
android:clickable="true"
android:clipChildren="false"
android:focusable="true"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="340dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/player_overlay_panel_background"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="48dp"
android:paddingTop="25dp"
android:paddingTop="20dp"
android:paddingEnd="48dp"
android:paddingBottom="22dp">
android:paddingBottom="34dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.14"
android:text="@string/player_now_playing"
android:text="@string/player_cast_eyebrow"
android:textColor="#FF52B54B"
android:textSize="11sp"
android:textStyle="bold" />
<!-- The title, not the word "Cast". The eyebrow above already says what the panel
is, and a viewer who opened this mid-scene wants confirmation of what they are
watching more than a heading repeating the button they just pressed. -->
<TextView
android:layout_width="wrap_content"
android:id="@+id/player_cast_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/player_cast"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#FFFFFFFF"
android:textSize="28sp"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/player_cast_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginTop="14dp"
android:text="@string/player_cast_loading"
android:textColor="#AFFFFFFF"
android:textSize="14sp" />
<HorizontalScrollView
android:id="@+id/player_cast_scroller"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="14dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipChildren="false"
android:clipToPadding="false"
android:fillViewport="false"
android:overScrollMode="never">
android:overScrollMode="never"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/player_cast_people"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:gravity="top"
android:orientation="horizontal" />
</HorizontalScrollView>
@@ -66,8 +81,10 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:letterSpacing="0.08"
android:text="@string/player_back_to_close"
android:textColor="#78FFFFFF"
android:textSize="12sp" />
android:textColor="#6EFFFFFF"
android:textSize="11sp" />
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- What a press of Left or Right just did, and where it lands. It sits over somebody's
film, so it is near-black, non-focusable and centred low on the screen where the
transport controls would be — the viewer is already looking there. It never opens the
transport OSD: skipping is meant to cost one press, not one press plus a menu. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_seek_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="104dp"
android:focusable="false"
android:visibility="gone">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/time_remaining_cue_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="18dp"
android:paddingTop="12dp"
android:paddingEnd="22dp"
android:paddingBottom="12dp">
<ImageView
android:id="@+id/player_seek_glyph"
android:layout_width="26dp"
android:layout_height="26dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/ic_player_forward" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:orientation="vertical">
<TextView
android:id="@+id/player_seek_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFFFF"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Forward 30 seconds" />
<TextView
android:id="@+id/player_seek_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="#99FFFFFF"
android:textSize="13sp"
tools:text="12:34 / 1:45:00" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -1,107 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- A drop-up over the subtitle button rather than a full-height panel: the film keeps
playing behind it, so there is no scrim and nothing bigger than the question being
asked. The margins line the panel up with the transport row's end inset (48dp) and
clear its 72dp button strip plus the controls' 28dp bottom padding; the top margin is
what caps how tall the track list can grow before it scrolls.
The panel holds two screens, not three sections. Choosing a track and fetching one the
title does not have are separate questions, and at 344dp by a third of a 720p screen
there is not room to ask both — stacking them squeezed the track list to a single row.
So `player_subtitle_main_section` and the results half of the download section swap:
the search row is the way in, Back is the way out, one press per level. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/player_subtitle_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#70000000"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="true"
android:visibility="gone">
<LinearLayout
android:layout_width="520dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginStart="30dp"
android:background="@drawable/player_overlay_panel_background"
android:id="@+id/player_subtitle_menu"
android:layout_width="344dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginTop="96dp"
android:layout_marginEnd="44dp"
android:layout_marginBottom="112dp"
android:background="@drawable/player_menu_background"
android:orientation="vertical"
android:paddingStart="34dp"
android:paddingTop="32dp"
android:paddingEnd="34dp"
android:paddingBottom="26dp">
android:paddingStart="10dp"
android:paddingTop="14dp"
android:paddingEnd="10dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.14"
android:text="@string/player_playback_options"
android:textColor="#FF52B54B"
android:textSize="11sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:text="@string/player_subtitles"
android:textColor="#FFFFFFFF"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
<LinearLayout
android:id="@+id/player_subtitle_main_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:lineSpacingExtra="2dp"
android:text="@string/player_subtitle_overlay_hint"
android:textColor="#AFFFFFFF"
android:textSize="14sp" />
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:layout_weight="1"
android:clipToPadding="false"
android:fillViewport="true"
android:overScrollMode="never">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:letterSpacing="0.12"
android:text="@string/player_subtitle_track"
android:textColor="#7AFFFFFF"
android:textSize="11sp"
android:textStyle="bold" />
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.12"
android:text="@string/player_subtitle_track"
android:textColor="#8FFFFFFF"
android:textSize="11sp"
android:textStyle="bold" />
android:layout_marginTop="8dp"
android:layout_weight="1"
android:fillViewport="false"
android:overScrollMode="never"
android:scrollbars="none">
<LinearLayout
android:id="@+id/player_subtitle_tracks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
android:orientation="vertical" />
</ScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:letterSpacing="0.12"
android:text="@string/player_text_size"
android:textColor="#8FFFFFFF"
android:textSize="11sp"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="14dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="14dp"
android:background="#14FFFFFF" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="12dp"
android:letterSpacing="0.12"
android:text="@string/player_text_size"
android:textColor="#7AFFFFFF"
android:textSize="11sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/player_subtitle_sizes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal" />
</LinearLayout>
<!-- Fetching a subtitle the title does not have. The whole group is GONE on a
backend that cannot do it, rather than showing a row that leads nowhere. -->
<LinearLayout
android:id="@+id/player_subtitle_download_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:visibility="gone">
<!-- Hidden with the section above it: with the menu swapped out this heading is
the first thing on the panel, and a rule above the first thing is a line
under the panel's own top edge. -->
<View
android:id="@+id/player_subtitle_download_rule"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="14dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="14dp"
android:background="#14FFFFFF" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="12dp"
android:letterSpacing="0.12"
android:text="@string/player_subtitle_download"
android:textColor="#7AFFFFFF"
android:textSize="11sp"
android:textStyle="bold" />
<TextView
android:id="@+id/player_subtitle_download_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="14dp"
android:textColor="#96FFFFFF"
android:textSize="12sp"
android:visibility="gone" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:fillViewport="false"
android:overScrollMode="never"
android:scrollbars="none">
<LinearLayout
android:id="@+id/player_subtitle_sizes"
android:id="@+id/player_subtitle_downloads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/player_back_to_close"
android:textColor="#78FFFFFF"
android:textSize="12sp" />
</ScrollView>
</LinearLayout>
</LinearLayout>
</FrameLayout>
+12 -4
View File
@@ -23,14 +23,20 @@
<string name="player_live">Live</string>
<string name="player_back">Back to previous screen</string>
<string name="player_hide_controls">Hide controls</string>
<string name="player_playback_options">PLAYBACK OPTIONS</string>
<string name="player_subtitles">Subtitles</string>
<string name="player_cast">Cast</string>
<string name="player_cast_eyebrow">CAST</string>
<string name="player_cast_loading">Loading cast…</string>
<string name="player_cast_empty">No cast information is available.</string>
<string name="player_subtitle_overlay_hint">Choose a track and tune the text size without leaving playback.</string>
<string name="player_subtitle_track">SUBTITLE TRACK</string>
<string name="player_subtitle_track">SUBTITLES</string>
<string name="player_text_size">TEXT SIZE</string>
<string name="player_subtitle_download">GET SUBTITLES</string>
<string name="player_subtitle_search">Search for subtitles…</string>
<string name="player_subtitle_search_again">Search again</string>
<string name="player_subtitle_searching">Looking for subtitles. This can take a moment.</string>
<string name="player_subtitle_search_empty">No subtitles were found for this release.</string>
<string name="player_subtitle_downloading">Downloading %1$s subtitles…</string>
<string name="player_subtitle_download_failed">That subtitle could not be downloaded. Try another.</string>
<string name="player_cast_open">Cast</string>
<string name="player_back_to_close">BACK · CLOSE</string>
<string name="player_ends_at">Ends at %1$s</string>
<string name="player_preroll_countdown_initial">Starting in 7 seconds…</string>
@@ -51,6 +57,8 @@
<item quantity="one">%1$d min</item>
<item quantity="other">%1$d mins</item>
</plurals>
<string name="player_seek_forward">Forward %1$s</string>
<string name="player_seek_back">Back %1$s</string>
<string name="next_up_label">NEXT UP</string>
<string name="next_up_play_now">Play now</string>
<string name="next_up_dismiss">Dismiss</string>
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- Matches context.cacheDir, where the update APK is downloaded. -->
<cache-path name="apk_cache" path="." />
</paths>