From dffcdef9456d325e7e70b379652c7412339e0328 Mon Sep 17 00:00:00 2001 From: Felix Schneider Date: Sun, 31 May 2026 12:10:22 +0200 Subject: [PATCH 1/3] feat(i18n): Add translation for "ends at" (#1474) Co-authored-by: Gauvain --- components/video-player/controls/TimeDisplay.tsx | 5 ++++- translations/de.json | 3 ++- translations/en.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/components/video-player/controls/TimeDisplay.tsx b/components/video-player/controls/TimeDisplay.tsx index 3f8cfd69e..f85af6343 100644 --- a/components/video-player/controls/TimeDisplay.tsx +++ b/components/video-player/controls/TimeDisplay.tsx @@ -1,4 +1,5 @@ import type { FC } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { Text } from "@/components/common/Text"; import { formatTimeString } from "@/utils/time"; @@ -16,6 +17,8 @@ export const TimeDisplay: FC = ({ currentTime, remainingTime, }) => { + const { t } = useTranslation(); + const getFinishTime = () => { const now = new Date(); // remainingTime is in ms @@ -37,7 +40,7 @@ export const TimeDisplay: FC = ({ -{formatTimeString(remainingTime, "ms")} - ends at {getFinishTime()} + {t("player.ends_at", { time: getFinishTime() })} diff --git a/translations/de.json b/translations/de.json index 3d3001dcd..87df58142 100644 --- a/translations/de.json +++ b/translations/de.json @@ -608,7 +608,8 @@ "downloaded_file_message": "Heruntergeladene Datei abspielen?", "downloaded_file_yes": "Ja", "downloaded_file_no": "Nein", - "downloaded_file_cancel": "Abbrechen" + "downloaded_file_cancel": "Abbrechen", + "ends_at": "Endet um {{time}}" }, "item_card": { "next_up": "Als NΓ€chstes", diff --git a/translations/en.json b/translations/en.json index 58fe4828b..320526063 100644 --- a/translations/en.json +++ b/translations/en.json @@ -698,7 +698,7 @@ "downloaded_file_no": "No", "downloaded_file_cancel": "Cancel", "swipe_down_settings": "Swipe down for settings", - "ends_at": "ends at", + "ends_at": "Ends at {{time}}", "search_subtitles": "Search Subtitles", "subtitle_tracks": "Tracks", "subtitle_search": "Search & Download", From ea5a999f213105e079df7733f954fee04f6702a0 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 12:40:56 +0200 Subject: [PATCH 2/3] fix(deps): declare react-native-tab-view and material-top-tabs (#1617) --- bun.lock | 28 ++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 30 insertions(+) diff --git a/bun.lock b/bun.lock index aa50de168..70f387822 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@gorhom/bottom-sheet": "5.2.8", "@jellyfin/sdk": "^0.13.0", "@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", "@tanstack/query-sync-storage-persister": "^5.100.14", @@ -83,6 +84,7 @@ "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", "react-native-svg": "15.15.4", + "react-native-tab-view": "4.3.0", "react-native-text-ticker": "^1.15.0", "react-native-track-player": "github:lovegaoshi/react-native-track-player#APM", "react-native-udp": "^4.1.7", @@ -537,6 +539,10 @@ "@react-navigation/core": ["@react-navigation/core@7.17.5", "", { "dependencies": { "@react-navigation/routers": "^7.5.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg=="], + "@react-navigation/elements": ["@react-navigation/elements@2.9.19", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.5", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-gBUvCZuUkOGw1KpLQEZIkByUz8RYPwXeoA6mZFJy9K1mxd8GdqHDMFCIoB0lfPz9rgrHj99RvtdlGZ/ZzkZv2A=="], + + "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.4.28", "", { "dependencies": { "@react-navigation/elements": "^2.9.19", "color": "^4.2.3", "react-native-tab-view": "^4.3.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.5", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-WZHJSGV2PQOD2Vr9LF8apGvcsbDKukzF3Fhh8xVNIesqaSi9TPProv4dRw6YkenUkjvFVZYkOjvwAJOToePVpA=="], + "@react-navigation/native": ["@react-navigation/native@7.2.5", "", { "dependencies": { "@react-navigation/core": "^7.17.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg=="], "@react-navigation/routers": ["@react-navigation/routers@7.5.5", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ=="], @@ -1589,6 +1595,8 @@ "react-native-svg": ["react-native-svg@15.15.4", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A=="], + "react-native-tab-view": ["react-native-tab-view@4.3.0", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-qPMF75uz/7+MuVG2g+YETdGMzlWZnhC6iI4h/7EBbwIBwNBIBi2z4OA6KhY3IOOBwGHXEIz5IyA6doDqifYBHg=="], + "react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="], "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd"], @@ -2001,6 +2009,10 @@ "@react-native/metro-babel-transformer/@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@react-navigation/elements/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "@react-navigation/material-top-tabs/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -2219,6 +2231,14 @@ "@react-native-community/cli-server-api/open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="], + "@react-navigation/elements/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@react-navigation/elements/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "@react-navigation/material-top-tabs/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@react-navigation/material-top-tabs/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -2331,6 +2351,14 @@ "@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "@react-navigation/elements/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@react-navigation/elements/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@react-navigation/material-top-tabs/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@react-navigation/material-top-tabs/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], diff --git a/package.json b/package.json index 74bcd674d..8bc2b3c64 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@gorhom/bottom-sheet": "5.2.8", "@jellyfin/sdk": "^0.13.0", "@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", "@tanstack/query-sync-storage-persister": "^5.100.14", @@ -104,6 +105,7 @@ "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", "react-native-svg": "15.15.4", + "react-native-tab-view": "4.3.0", "react-native-text-ticker": "^1.15.0", "react-native-track-player": "github:lovegaoshi/react-native-track-player#APM", "react-native-udp": "^4.1.7", From eb8dd51b4e4393c3a7389d8258a644e6b322774a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 13:23:02 +0200 Subject: [PATCH 3/3] feat(ci): EAS build + auto-submit release workflow for main (#1616) --- .github/workflows/release.yml | 132 ++++++++++++++++++++++++++++++++++ .gitignore | 6 ++ eas.json | 11 ++- 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..1e9da617f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: πŸš€ Release (EAS Build + Submit) + +# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main. +# A manual approval gate (the `production` GitHub Environment) pauses the run +# before any build/submit starts. Configure required reviewers on that +# environment in repo Settings β†’ Environments β†’ production. + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + approve: + name: πŸ” Approve release + runs-on: ubuntu-24.04 + environment: production + steps: + - name: βœ… Release approved + run: echo "Release approved for ${{ github.sha }}" + + release: + name: πŸš€ ${{ matrix.name }} + needs: approve + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - name: 🍎 iOS + platform: ios + profile: production + - name: πŸ“Ί tvOS + platform: ios + profile: production_tv + - name: πŸ€– Android + platform: android + profile: production + + steps: + - name: πŸ“₯ Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: πŸ’Ύ Cache Bun dependencies + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-cache + + - name: πŸ“¦ Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: πŸ—οΈ Setup EAS + uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true + + # tvOS uses local credentials (EAS can't manage tvOS provisioning + # remotely, including the TopShelf extension target). Restore the + # gitignored credentials.json + cert + profiles from secrets so the + # cloud build can sign with `credentialsSource: local`. + - name: πŸ” Restore tvOS signing credentials + if: matrix.profile == 'production_tv' + env: + EAS_CREDENTIALS_JSON: ${{ secrets.EAS_CREDENTIALS_JSON }} + TVOS_DIST_CERT_P12_BASE64: ${{ secrets.TVOS_DIST_CERT_P12_BASE64 }} + TVOS_APP_PROFILE_BASE64: ${{ secrets.TVOS_APP_PROFILE_BASE64 }} + TVOS_TOPSHELF_PROFILE_BASE64: ${{ secrets.TVOS_TOPSHELF_PROFILE_BASE64 }} + run: | + mkdir -p certs profiles + printf '%s' "$EAS_CREDENTIALS_JSON" > credentials.json + echo "$TVOS_DIST_CERT_P12_BASE64" | base64 -d > certs/distribution.p12 + echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision + echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision + + # iOS + tvOS submit upload to App Store Connect with an ASC API key. + # EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID / + # EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8, + # tolerating either raw-PEM or base64-encoded secret content. + - name: πŸ” Restore App Store Connect API key + if: matrix.platform == 'ios' + env: + APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} + run: | + if printf '%s' "$APPLE_KEY_CONTENT" | grep -q "BEGIN PRIVATE KEY"; then + printf '%s' "$APPLE_KEY_CONTENT" > "$RUNNER_TEMP/asc_api_key.p8" + else + printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8" + fi + + # Android submit needs a Google Play service account JSON. eas.json's + # submit.production.android.serviceAccountKeyPath points at this file. + - name: πŸ” Restore Google Play service account + if: matrix.platform == 'android' + env: + GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json + + - name: πŸš€ Build & submit (${{ matrix.name }}) + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + # Consumed by eas submit for iOS/tvOS; ignored for Android. + EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8 + EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }} + run: | + eas build \ + --platform ${{ matrix.platform }} \ + --profile ${{ matrix.profile }} \ + --auto-submit \ + --non-interactive diff --git a/.gitignore b/.gitignore index c39e191b9..46328035d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ web-build/ /androidmobile /androidtv +# Gradle caches (top-level + per-module native projects) +**/.gradle/ + # Module-specific Builds modules/mpv-player/android/build modules/player/android @@ -76,3 +79,6 @@ build/ .claude/ .agents/skills/** skills-lock.json + +# CI-injected Google Play service account key (written at build time) +google-service-account.json diff --git a/eas.json b/eas.json index afc774889..c4c2b0e07 100644 --- a/eas.json +++ b/eas.json @@ -1,6 +1,6 @@ { "cli": { - "version": ">= 9.1.0", + "version": ">= 16.0.0", "appVersionSource": "remote" }, "build": { @@ -52,6 +52,7 @@ } }, "production": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "android": { @@ -59,6 +60,7 @@ } }, "production-apk": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "android": { @@ -67,6 +69,7 @@ } }, "production-apk-tv": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "android": { @@ -78,6 +81,7 @@ } }, "production_tv": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "env": { @@ -93,6 +97,11 @@ "ios": { "appleTeamId": "MWD5K362T8", "ascAppId": "6593660679" + }, + "android": { + "serviceAccountKeyPath": "./google-service-account.json", + "track": "internal", + "releaseStatus": "completed" } }, "production_tv": {