From 18f01fa4ab39b92a5456143303221b07a15cdb26 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:46:54 +0200 Subject: [PATCH 01/10] chore(deps): Update actions/checkout action to v7 (#1746) --- .github/workflows/build-apps.yml | 12 ++++++------ .github/workflows/check-lockfile.yml | 2 +- .github/workflows/ci-codeql.yml | 2 +- .github/workflows/crowdin.yml | 2 +- .github/workflows/detect-duplicate.yml | 2 +- .github/workflows/linting.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- .github/workflows/trivy-scan.yml | 2 +- .github/workflows/update-issue-form.yml | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index 3a50064c..02cb46a7 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -43,7 +43,7 @@ jobs: swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -143,7 +143,7 @@ jobs: swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -230,7 +230,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -302,7 +302,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -369,7 +369,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -439,7 +439,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index efb5f221..b140e33d 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -19,7 +19,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} show-progress: false diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index b77665f5..b9921780 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -27,7 +27,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 🏁 Initialize CodeQL uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 39883d8c..c14fe48f 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -23,7 +23,7 @@ jobs: steps: - name: 📥 Checkout Repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index 26da4f57..ebf515d7 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -21,7 +21,7 @@ jobs: contents: read steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index d36da31f..540c9a55 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -51,7 +51,7 @@ jobs: contents: read steps: - name: Checkout Repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-26.04 steps: - name: 🛒 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -104,7 +104,7 @@ jobs: steps: - name: "📥 Checkout PR code" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 454f8645..1dbad1b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 submodules: recursive @@ -184,7 +184,7 @@ jobs: actions: read # required for `gh run download` to list/fetch this run's artifacts steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 show-progress: false diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 2f02dcfc..2e0f307b 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -27,7 +27,7 @@ jobs: security-events: write # upload SARIF to code scanning steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # Trivy's own action caches the vulnerability DB + binary internally # (cache-trivy-* / trivy-binary-* entries), so no manual ~/.cache/trivy diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 0754735e..7f1ace97 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -26,7 +26,7 @@ jobs: pull-requests: write steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # On `release` events GITHUB_SHA is the tagged commit — without this the # script would regenerate the form from the tag's (stale) copy and the bot From 2ec65944627d546f6c0f3cdf2674d441eb9487a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:19:10 +0200 Subject: [PATCH 02/10] chore(deps): Update CI dependencies to v24.17.0 (#1745) --- .github/workflows/linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 540c9a55..d2c70d5a 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -114,7 +114,7 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: # renovate: datasource=node-version depName=node versioning=node - node-version: "24.16.0" + node-version: "24.17.0" - name: "🍞 Setup Bun" uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 From ce66f0256e0fc5da0ec86a3f7374c7a27841a0e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:53:02 +0200 Subject: [PATCH 03/10] chore(deps): Update dependency @shopify/flash-list to v2.0.3 (#1629) --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 8ca83cb3..a50086e8 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ "@react-native-community/netinfo": "^12.0.0", "@react-navigation/material-top-tabs": "7.4.28", "@react-navigation/native": "^7.2.5", - "@shopify/flash-list": "2.0.2", + "@shopify/flash-list": "2.0.3", "@tanstack/query-sync-storage-persister": "^5.100.14", "@tanstack/react-pacer": "^0.19.1", "@tanstack/react-query": "5.100.14", @@ -536,7 +536,7 @@ "@react-navigation/routers": ["@react-navigation/routers@7.6.0", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow=="], - "@shopify/flash-list": ["@shopify/flash-list@2.0.2", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w=="], + "@shopify/flash-list": ["@shopify/flash-list@2.0.3", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-jUlHuZFoPdqRCDvOqsb2YkTttRPyV8Tb/EjCx3gE2wjr4UTM+fE0Ltv9bwBg0K7yo/SxRNXaW7xu5utusRb0xA=="], "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], diff --git a/package.json b/package.json index fbf33a84..2f5260cc 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@react-native-community/netinfo": "^12.0.0", "@react-navigation/material-top-tabs": "7.4.28", "@react-navigation/native": "^7.2.5", - "@shopify/flash-list": "2.0.2", + "@shopify/flash-list": "2.0.3", "@tanstack/query-sync-storage-persister": "^5.100.14", "@tanstack/react-pacer": "^0.19.1", "@tanstack/react-query": "5.100.14", From e660b9887182f47d29cac379b99f6e5f0304b1c6 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Fri, 19 Jun 2026 00:12:40 +0200 Subject: [PATCH 04/10] fix(mpv): force software decoding on Android emulator (#1752) --- .../modules/mpvplayer/MPVLayerRenderer.kt | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt index 8b6808fd..93776d10 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt @@ -4,6 +4,7 @@ import android.app.UiModeManager import android.content.Context import android.content.res.Configuration import android.content.res.AssetManager +import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log @@ -35,6 +36,30 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION } + /** + * True only on the Android emulator. Its goldfish/ranchu MediaCodec can't bind a + * decode output surface (decode opens with surface 0x0): HEVC then fails cleanly and + * mpv auto-falls-back to software, but H.264 "opens" deceptively and wedges the core + * (no fallback) — black video, then any command (seek/pause) deadlocks the UI thread + * → ANR. We force software decoding here. + * + * Only QEMU/SDK-exclusive signals are checked so a real device can never match — a + * false positive would needlessly drop shipping hardware to software decoding. The + * emulator reports ro.hardware=goldfish|ranchu, an sdk_* product, or a generic/ + * emulator build fingerprint, none of which appear on real devices. + */ + private fun isEmulator(): Boolean { + val hardware = Build.HARDWARE.lowercase() + if (hardware == "goldfish" || hardware == "ranchu") return true + + val product = Build.PRODUCT + if (product == "sdk" || product.startsWith("sdk_")) return true + + val fingerprint = Build.FINGERPRINT + return fingerprint.startsWith("generic") || + fingerprint.contains("emulator", ignoreCase = true) + } + interface Delegate { fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) fun onPauseChanged(isPaused: Boolean) @@ -169,15 +194,21 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.setOptionString("gpu-context", "android") MPVLib.setOptionString("opengl-es", "yes") - // Hardware video decoding - // TV: zero-copy (mediacodec) for better performance on low-power devices - // Mobile: copy mode (mediacodec-copy) for better compatibility - val isTV = isTvDevice() - if (isTV) { - MPVLib.setOptionString("hwdec", "mediacodec") - MPVLib.setOptionString("profile", "fast") - } else { - MPVLib.setOptionString("hwdec", "mediacodec-copy") + // Hardware decode path: + // - Real TV hardware: zero-copy `mediacodec` (fastest on low-power devices). + // - Real phone: `mediacodec-copy` (broadest compatibility). + // - Emulator: software decode. Its MediaCodec can't bind an output surface + // (surface 0x0); HEVC then fails cleanly and mpv auto-falls-back to software, + // but H.264 "opens" deceptively and wedges the core with no fallback (black + // video, then any command — seek/pause — deadlocks the UI thread → ANR). + // hwdec=no makes every codec render via the gpu-next VO. Real devices unaffected. + when { + isEmulator() -> MPVLib.setOptionString("hwdec", "no") + isTvDevice() -> { + MPVLib.setOptionString("hwdec", "mediacodec") + MPVLib.setOptionString("profile", "fast") + } + else -> MPVLib.setOptionString("hwdec", "mediacodec-copy") } MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1") From b256e99fc8fc00babf5f029cce3fd487ba6b126e Mon Sep 17 00:00:00 2001 From: Niyazaki <34636175+niyazaki@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:11:38 +0200 Subject: [PATCH 05/10] fix(search): set typed text color on Android search bar (#1756) --- app/(auth)/(tabs)/(search)/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 29461b49..759dae85 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -305,6 +305,8 @@ export default function SearchPage() { }, hideWhenScrolling: false, autoFocus: false, + // Android: color of the user-typed text (was dark and unreadable on the dark header) + textColor: "#fff", // Android: placeholder and icon color hintTextColor: "#fff", headerIconColor: "#fff", From 517bc7bbb5aa7a4b7cde13d8b55837b9ad2ab3d2 Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:08:12 +0200 Subject: [PATCH 06/10] feat: android tv menu (#1709) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/(tabs)/_layout.tsx | 109 +++++++++++++++++++- components/home/TVHeroCarousel.tsx | 2 +- components/tv/TVNavBar.tsx | 155 +++++++++++++++++++++++++++++ components/tv/index.ts | 2 + hooks/useTVBackHandler.ts | 65 ++++++++---- 5 files changed, 307 insertions(+), 26 deletions(-) create mode 100644 components/tv/TVNavBar.tsx diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 53fbeb91..45f246a5 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -3,16 +3,24 @@ import { type NativeBottomTabNavigationEventMap, type NativeBottomTabNavigationOptions, } from "@bottom-tabs/react-navigation"; -import { withLayoutContext } from "expo-router"; +import { Stack, useSegments, withLayoutContext } from "expo-router"; import type { ParamListBase, TabNavigationState, } from "expo-router/react-navigation"; +import { useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; +import type { TVNavBarTab } from "@/components/tv/TVNavBar"; +import { TVNavBar } from "@/components/tv/TVNavBar"; import { Colors } from "@/constants/Colors"; -import { useTVHomeBackHandler } from "@/hooks/useTVBackHandler"; +import useRouter from "@/hooks/useAppRouter"; +import { + isTabRoute, + useTVHomeBackHandler, + useTVTabRootBackHandler, +} from "@/hooks/useTVBackHandler"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; @@ -33,13 +41,108 @@ export const NativeTabs = withLayoutContext< NativeBottomTabNavigationEventMap >(Navigator); +const IS_ANDROID_TV = Platform.isTV && Platform.OS === "android"; + +function TVTabLayout() { + const { settings } = useSettings(); + const { t } = useTranslation(); + const segments = useSegments(); + const router = useRouter(); + + const currentTab = segments.find(isTabRoute); + const lastSegment = segments[segments.length - 1] ?? ""; + const atTabRoot = isTabRoute(lastSegment) || lastSegment === "index"; + + const tabs: TVNavBarTab[] = useMemo( + () => + [ + { key: "(home)", label: t("tabs.home") }, + { key: "(search)", label: t("tabs.search") }, + { key: "(favorites)", label: t("tabs.favorites") }, + !settings?.streamyStatsServerUrl || settings?.hideWatchlistsTab + ? null + : { key: "(watchlists)", label: t("watchlists.title") }, + { key: "(libraries)", label: t("tabs.library") }, + !settings?.showCustomMenuLinks + ? null + : { key: "(custom-links)", label: t("tabs.custom_links") }, + { key: "(settings)", label: t("tabs.settings") }, + ].filter((tab): tab is TVNavBarTab => tab !== null), + [ + settings?.streamyStatsServerUrl, + settings?.hideWatchlistsTab, + settings?.showCustomMenuLinks, + t, + ], + ); + + const activeTabKey = currentTab ?? "(home)"; + + const visibleKeys = useMemo( + () => new Set(tabs.map((tab) => tab.key)), + [tabs], + ); + + const handleTabChange = useCallback( + (key: string) => { + if (key === currentTab) return; + + if (key === "(home)") eventBus.emit("scrollToTop"); + if (key === "(search)") eventBus.emit("searchTabPressed"); + + router.replace(`/(auth)/(tabs)/${key}`); + }, + [currentTab, router], + ); + + const navigateHome = useCallback(() => { + router.replace("/(auth)/(tabs)/(home)"); + }, [router]); + useTVTabRootBackHandler(navigateHome, atTabRoot, currentTab); + + // If current tab is no longer visible (setting changed), navigate to home + useEffect(() => { + if (!visibleKeys.has(activeTabKey) && activeTabKey !== "(home)") { + router.replace("/(auth)/(tabs)/(home)"); + } + }, [visibleKeys, activeTabKey, router]); + + return ( + + + ); +} + export default function TabLayout() { const { settings } = useSettings(); const { t } = useTranslation(); - // Handle TV back button - prevent app exit when at root + // Must be called before any conditional return (rules of hooks) useTVHomeBackHandler(); + if (IS_ANDROID_TV) { + return ; + } + return (