diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index 02cb46a7..15a7b03a 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -57,7 +57,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} @@ -79,7 +79,7 @@ jobs: java-version: "17" - name: 💾 Cache Gradle global - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: | ~/.gradle/caches/modules-2 @@ -92,7 +92,7 @@ jobs: run: bun run prebuild - name: 💾 Cache project Gradle (.gradle) - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: android/.gradle key: ${{ runner.os }}-${{ runner.arch }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} @@ -157,7 +157,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} @@ -179,7 +179,7 @@ jobs: java-version: "17" - name: 💾 Cache Gradle global - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: | ~/.gradle/caches/modules-2 @@ -192,7 +192,7 @@ jobs: run: bun run prebuild:tv - name: 💾 Cache project Gradle (.gradle) - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: android/.gradle key: ${{ runner.os }}-${{ runner.arch }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} @@ -244,7 +244,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} @@ -316,7 +316,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} @@ -383,7 +383,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} @@ -453,7 +453,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index b140e33d..d4165055 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -33,7 +33,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: | ~/.bun/install/cache diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index d2c70d5a..f8799f26 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.17.0" + node-version: "24.18.0" - name: "🍞 Setup Bun" uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1dbad1b5..027eab0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,7 @@ jobs: bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} diff --git a/README.md b/README.md index 258005ef..3d4221f4 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Thanks to [@Alexk2309](https://github.com/Alexk2309) for the hard work building ## 🛣️ Roadmap -Check out our [Roadmap](https://github.com/users/fredrikburmester/projects/5) To see what we're working on next, we are always open to feedback and suggestions. Please let us know if you have any ideas or feature requests. +Check out our [Roadmap](https://github.com/orgs/streamyfin/projects/3/views/1) to see what we're working on next, we are always open to feedback and suggestions. Please let us know if you have any ideas or feature requests. ## 📥 Download Streamyfin 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 (