mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-23 15:26:42 +01:00
Compare commits
26 Commits
refactor-c
...
autoskip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfb9ce3158 | ||
|
|
0aa2dc5924 | ||
|
|
0990e479d2 | ||
|
|
e7f200a114 | ||
|
|
da9afacbf7 | ||
|
|
80fdd579f3 | ||
|
|
63ed386369 | ||
|
|
0a58514964 | ||
|
|
58f8015e3b | ||
|
|
a27ea154ba | ||
|
|
6c3fa704db | ||
|
|
fe315699b9 | ||
|
|
c3271859b8 | ||
|
|
294b3f19c3 | ||
|
|
e9bb6b3c40 | ||
|
|
9d437e8cd1 | ||
|
|
ebf6e31478 | ||
|
|
378288bf08 | ||
|
|
92460cf202 | ||
|
|
feb5a41cff | ||
|
|
97607b2263 | ||
|
|
d3bc2ac5d5 | ||
|
|
96f6ad000b | ||
|
|
be575b7c04 | ||
|
|
91de36c3bd | ||
|
|
62f50590d4 |
73
.github/workflows/artifact-comment.yml
vendored
73
.github/workflows/artifact-comment.yml
vendored
@@ -188,6 +188,17 @@ jobs:
|
|||||||
if (latestAppsRun) {
|
if (latestAppsRun) {
|
||||||
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
|
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
|
||||||
|
|
||||||
|
// Map job names to our build targets. Declared outside the try so
|
||||||
|
// the catch fallback can reuse the same keys.
|
||||||
|
const jobMappings = {
|
||||||
|
'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
|
||||||
|
'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
|
||||||
|
'iOS': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'],
|
||||||
|
'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)', 'build-ios-phone-unsigned'],
|
||||||
|
'tvOS': ['🍎 Build tvOS IPA', 'build-ios-tv'],
|
||||||
|
'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)', 'build-ios-tv-unsigned']
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all jobs for this workflow run
|
// Get all jobs for this workflow run
|
||||||
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||||
@@ -216,13 +227,6 @@ jobs:
|
|||||||
return; // Exit early
|
return; // Exit early
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map job names to our build targets
|
|
||||||
const jobMappings = {
|
|
||||||
'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
|
|
||||||
'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
|
|
||||||
'iOS Phone': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone']
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create individual status for each job
|
// Create individual status for each job
|
||||||
for (const [platform, jobNames] of Object.entries(jobMappings)) {
|
for (const [platform, jobNames] of Object.entries(jobMappings)) {
|
||||||
const job = jobs.jobs.find(j =>
|
const job = jobs.jobs.find(j =>
|
||||||
@@ -236,7 +240,9 @@ jobs:
|
|||||||
conclusion: job.conclusion,
|
conclusion: job.conclusion,
|
||||||
url: job.html_url,
|
url: job.html_url,
|
||||||
runId: latestAppsRun.id,
|
runId: latestAppsRun.id,
|
||||||
created_at: job.started_at || latestAppsRun.created_at
|
created_at: job.started_at || latestAppsRun.created_at,
|
||||||
|
started_at: job.started_at,
|
||||||
|
completed_at: job.completed_at
|
||||||
};
|
};
|
||||||
console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`);
|
console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`);
|
||||||
} else {
|
} else {
|
||||||
@@ -247,22 +253,30 @@ jobs:
|
|||||||
conclusion: latestAppsRun.conclusion,
|
conclusion: latestAppsRun.conclusion,
|
||||||
url: latestAppsRun.html_url,
|
url: latestAppsRun.html_url,
|
||||||
runId: latestAppsRun.id,
|
runId: latestAppsRun.id,
|
||||||
created_at: latestAppsRun.created_at
|
created_at: latestAppsRun.created_at,
|
||||||
|
started_at: latestAppsRun.run_started_at,
|
||||||
|
completed_at: latestAppsRun.updated_at
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message);
|
console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message);
|
||||||
// Fallback to workflow-level status
|
// Fallback to workflow-level status for every build target.
|
||||||
buildStatuses['Android Phone'] = buildStatuses['Android TV'] = buildStatuses['iOS Phone'] = {
|
// Keys must match jobMappings / buildTargets statusKey values.
|
||||||
|
const fallbackStatus = {
|
||||||
name: latestAppsRun.name,
|
name: latestAppsRun.name,
|
||||||
status: latestAppsRun.status,
|
status: latestAppsRun.status,
|
||||||
conclusion: latestAppsRun.conclusion,
|
conclusion: latestAppsRun.conclusion,
|
||||||
url: latestAppsRun.html_url,
|
url: latestAppsRun.html_url,
|
||||||
runId: latestAppsRun.id,
|
runId: latestAppsRun.id,
|
||||||
created_at: latestAppsRun.created_at
|
created_at: latestAppsRun.created_at,
|
||||||
|
started_at: latestAppsRun.run_started_at,
|
||||||
|
completed_at: latestAppsRun.updated_at
|
||||||
};
|
};
|
||||||
|
for (const platform of Object.keys(jobMappings)) {
|
||||||
|
buildStatuses[platform] = fallbackStatus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect artifacts if any job has completed successfully
|
// Collect artifacts if any job has completed successfully
|
||||||
@@ -353,10 +367,12 @@ jobs:
|
|||||||
|
|
||||||
// Process each expected build target individually
|
// Process each expected build target individually
|
||||||
const buildTargets = [
|
const buildTargets = [
|
||||||
{ name: 'Android Phone', platform: '🤖', device: '📱', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
|
{ name: 'Android Phone', platform: '🤖', device: '📱 Phone', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
|
||||||
{ name: 'Android TV', platform: '🤖', device: '📺', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
|
{ name: 'Android TV', platform: '🤖', device: '📺 TV', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
|
||||||
{ name: 'iOS Phone', platform: '🍎', device: '📱', statusKey: 'iOS Phone', artifactPattern: /ios.*phone/i },
|
{ name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS', artifactPattern: /ios.*phone.*ipa(?!.*unsigned)/i },
|
||||||
{ name: 'iOS TV', platform: '🍎', device: '📺', statusKey: 'iOS TV', artifactPattern: /ios.*tv/i }
|
{ name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Unsigned', artifactPattern: /ios.*phone.*unsigned/i },
|
||||||
|
{ name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /ios.*tv.*ipa(?!.*unsigned)/i },
|
||||||
|
{ name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const target of buildTargets) {
|
for (const target of buildTargets) {
|
||||||
@@ -371,16 +387,31 @@ jobs:
|
|||||||
let status = '⏳ Pending';
|
let status = '⏳ Pending';
|
||||||
let downloadLink = '*Waiting for build...*';
|
let downloadLink = '*Waiting for build...*';
|
||||||
|
|
||||||
// Special case for iOS TV - show as disabled
|
// tvOS builds are temporarily disabled until feat/tv-interface
|
||||||
if (target.name === 'iOS TV') {
|
// is merged - show them as disabled instead of stuck pending.
|
||||||
|
if (target.name === 'tvOS' || target.name === 'tvOS Unsigned') {
|
||||||
status = '💤 Disabled';
|
status = '💤 Disabled';
|
||||||
downloadLink = '*Disabled for now*';
|
downloadLink = '*Disabled until feat/tv-interface is merged*';
|
||||||
} else if (matchingStatus) {
|
} else if (matchingStatus) {
|
||||||
if (matchingStatus.conclusion === 'success' && matchingArtifact) {
|
if (matchingStatus.conclusion === 'success' && matchingArtifact) {
|
||||||
status = '✅ Complete';
|
status = '✅ Complete';
|
||||||
const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`;
|
const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`;
|
||||||
const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
|
const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
|
||||||
downloadLink = `[📥 Download ${fileType}](${directLink})`;
|
|
||||||
|
// Format file size
|
||||||
|
const sizeInMB = (matchingArtifact.size_in_bytes / (1024 * 1024)).toFixed(1);
|
||||||
|
const sizeInfo = `(${sizeInMB} MB)`;
|
||||||
|
|
||||||
|
// Calculate build duration
|
||||||
|
let durationInfo = '';
|
||||||
|
if (matchingStatus.started_at && matchingStatus.completed_at) {
|
||||||
|
const durationMs = new Date(matchingStatus.completed_at) - new Date(matchingStatus.started_at);
|
||||||
|
const durationMin = Math.floor(durationMs / 60000);
|
||||||
|
const durationSec = Math.floor((durationMs % 60000) / 1000);
|
||||||
|
durationInfo = ` - ${durationMin}m ${durationSec}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`;
|
||||||
} else if (matchingStatus.conclusion === 'failure') {
|
} else if (matchingStatus.conclusion === 'failure') {
|
||||||
status = `❌ [Failed](${matchingStatus.url})`;
|
status = `❌ [Failed](${matchingStatus.url})`;
|
||||||
downloadLink = '*Build failed*';
|
downloadLink = '*Build failed*';
|
||||||
@@ -408,7 +439,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`;
|
commentBody += `| ${target.platform} ${target.name} | ${target.device} | ${status} | ${downloadLink} |\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
commentBody += `\n`;
|
commentBody += `\n`;
|
||||||
|
|||||||
188
.github/workflows/build-apps.yml
vendored
188
.github/workflows/build-apps.yml
vendored
@@ -299,67 +299,127 @@ jobs:
|
|||||||
path: build/*.ipa
|
path: build/*.ipa
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
# Disabled for now - uncomment when ready to build iOS TV
|
build-ios-tv:
|
||||||
# build-ios-tv:
|
# Temporarily disabled until feat/tv-interface is merged (TV UI not ready).
|
||||||
# if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
# Re-enable by removing the `false &&` prefix below.
|
||||||
# runs-on: macos-26
|
if: false && (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
||||||
# name: 🍎 Build iOS IPA (TV)
|
runs-on: macos-26
|
||||||
# permissions:
|
name: 🍎 Build tvOS IPA
|
||||||
# contents: read
|
permissions:
|
||||||
#
|
contents: read
|
||||||
# steps:
|
|
||||||
# - name: 📥 Checkout code
|
steps:
|
||||||
# uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- name: 📥 Checkout code
|
||||||
# with:
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
with:
|
||||||
# fetch-depth: 0
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
# submodules: recursive
|
fetch-depth: 0
|
||||||
# show-progress: false
|
submodules: recursive
|
||||||
#
|
show-progress: false
|
||||||
# - name: 🍞 Setup Bun
|
|
||||||
# uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
- name: 🍞 Setup Bun
|
||||||
# with:
|
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
|
||||||
# bun-version: latest
|
with:
|
||||||
#
|
bun-version: latest
|
||||||
# - name: 💾 Cache Bun dependencies
|
|
||||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
- name: 💾 Cache Bun dependencies
|
||||||
# with:
|
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||||
# path: ~/.bun/install/cache
|
with:
|
||||||
# key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
path: ~/.bun/install/cache
|
||||||
# restore-keys: |
|
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||||
# ${{ runner.os }}-bun-cache
|
restore-keys: |
|
||||||
#
|
${{ runner.os }}-bun-cache
|
||||||
# - name: 📦 Install dependencies and reload submodules
|
|
||||||
# run: |
|
- name: 📦 Install dependencies and reload submodules
|
||||||
# bun install --frozen-lockfile
|
run: |
|
||||||
# bun run submodule-reload
|
bun install --frozen-lockfile
|
||||||
#
|
bun run submodule-reload
|
||||||
# - name: 🛠️ Generate project files
|
|
||||||
# run: bun run prebuild:tv
|
- name: 🛠️ Generate project files
|
||||||
#
|
run: bun run prebuild:tv
|
||||||
# - name: 🔧 Setup Xcode
|
|
||||||
# uses: maxim-lobanov/setup-xcode@v1
|
- name: 🔧 Setup Xcode
|
||||||
# with:
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
||||||
# xcode-version: '26.0.1'
|
with:
|
||||||
#
|
xcode-version: "26.2"
|
||||||
# - name: 🏗️ Setup EAS
|
|
||||||
# uses: expo/expo-github-action@main
|
- name: 🏗️ Setup EAS
|
||||||
# with:
|
uses: expo/expo-github-action@main
|
||||||
# eas-version: latest
|
with:
|
||||||
# token: ${{ secrets.EXPO_TOKEN }}
|
eas-version: latest
|
||||||
# eas-cache: true
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
#
|
eas-cache: true
|
||||||
# - name: 🚀 Build iOS app
|
|
||||||
# env:
|
- name: 🚀 Build iOS app
|
||||||
# EXPO_TV: 1
|
env:
|
||||||
# run: eas build -p ios --local --non-interactive
|
EXPO_TV: 1
|
||||||
#
|
run: eas build -p ios --local --non-interactive
|
||||||
# - name: 📅 Set date tag
|
|
||||||
# run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
- name: 📅 Set date tag
|
||||||
#
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
# - name: 📤 Upload IPA artifact
|
|
||||||
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
- name: 📤 Upload IPA artifact
|
||||||
# with:
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
# name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
|
with:
|
||||||
# path: build-*.ipa
|
name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
|
||||||
# retention-days: 7
|
path: build-*.ipa
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
build-ios-tv-unsigned:
|
||||||
|
# Temporarily disabled until feat/tv-interface is merged (TV UI not ready).
|
||||||
|
# Re-enable by removing the `false &&` prefix below.
|
||||||
|
if: false && (!contains(github.event.head_commit.message, '[skip ci]'))
|
||||||
|
runs-on: macos-26
|
||||||
|
name: 🍎 Build tvOS IPA (Unsigned)
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: 🍞 Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: 💾 Cache Bun dependencies
|
||||||
|
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||||
|
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: 🛠️ Generate project files
|
||||||
|
run: bun run prebuild:tv
|
||||||
|
|
||||||
|
- name: 🔧 Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
||||||
|
with:
|
||||||
|
xcode-version: "26.2"
|
||||||
|
|
||||||
|
- name: 🚀 Build iOS app
|
||||||
|
env:
|
||||||
|
EXPO_TV: 1
|
||||||
|
run: bun run ios:unsigned-build:tv ${{ github.event_name == 'pull_request' && '-- --verbose' || '' }}
|
||||||
|
|
||||||
|
- name: 📅 Set date tag
|
||||||
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 📤 Upload IPA artifact
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
|
with:
|
||||||
|
name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }}
|
||||||
|
path: build/*.ipa
|
||||||
|
retention-days: 7
|
||||||
|
|||||||
233
app/(auth)/(tabs)/(home)/settings/segment-skip/page.tsx
Normal file
233
app/(auth)/(tabs)/(home)/settings/segment-skip/page.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useNavigation } from "expo-router";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create skip options for a specific segment type
|
||||||
|
* Reduces code duplication across all 5 segment types
|
||||||
|
*/
|
||||||
|
const useSkipOptions = (
|
||||||
|
settingKey:
|
||||||
|
| "skipIntro"
|
||||||
|
| "skipOutro"
|
||||||
|
| "skipRecap"
|
||||||
|
| "skipCommercial"
|
||||||
|
| "skipPreview",
|
||||||
|
settings: ReturnType<typeof useSettings>["settings"] | null,
|
||||||
|
updateSettings: ReturnType<typeof useSettings>["updateSettings"],
|
||||||
|
t: TFunction<"translation", undefined>,
|
||||||
|
) => {
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
options: SEGMENT_SKIP_OPTIONS(t).map((option) => ({
|
||||||
|
type: "radio" as const,
|
||||||
|
label: option.label,
|
||||||
|
value: option.value,
|
||||||
|
selected: option.value === settings?.[settingKey],
|
||||||
|
onPress: () => updateSettings({ [settingKey]: option.value }),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[settings?.[settingKey], updateSettings, t, settingKey],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SegmentSkipPage() {
|
||||||
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
title: t("home.settings.other.segment_skip_settings"),
|
||||||
|
});
|
||||||
|
}, [navigation, t]);
|
||||||
|
|
||||||
|
const skipIntroOptions = useSkipOptions(
|
||||||
|
"skipIntro",
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
t,
|
||||||
|
);
|
||||||
|
const skipOutroOptions = useSkipOptions(
|
||||||
|
"skipOutro",
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
t,
|
||||||
|
);
|
||||||
|
const skipRecapOptions = useSkipOptions(
|
||||||
|
"skipRecap",
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
t,
|
||||||
|
);
|
||||||
|
const skipCommercialOptions = useSkipOptions(
|
||||||
|
"skipCommercial",
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
t,
|
||||||
|
);
|
||||||
|
const skipPreviewOptions = useSkipOptions(
|
||||||
|
"skipPreview",
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
t,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisabledSetting disabled={false} className='px-4'>
|
||||||
|
<ListGroup>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.skip_intro")}
|
||||||
|
subtitle={t("home.settings.other.skip_intro_description")}
|
||||||
|
disabled={pluginSettings?.skipIntro?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
groups={skipIntroOptions}
|
||||||
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(`home.settings.other.segment_skip_${settings.skipIntro}`)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.skip_intro")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.skip_outro")}
|
||||||
|
subtitle={t("home.settings.other.skip_outro_description")}
|
||||||
|
disabled={pluginSettings?.skipOutro?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
groups={skipOutroOptions}
|
||||||
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(`home.settings.other.segment_skip_${settings.skipOutro}`)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.skip_outro")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.skip_recap")}
|
||||||
|
subtitle={t("home.settings.other.skip_recap_description")}
|
||||||
|
disabled={pluginSettings?.skipRecap?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
groups={skipRecapOptions}
|
||||||
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(`home.settings.other.segment_skip_${settings.skipRecap}`)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.skip_recap")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.skip_commercial")}
|
||||||
|
subtitle={t("home.settings.other.skip_commercial_description")}
|
||||||
|
disabled={pluginSettings?.skipCommercial?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
groups={skipCommercialOptions}
|
||||||
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(
|
||||||
|
`home.settings.other.segment_skip_${settings.skipCommercial}`,
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.skip_commercial")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.skip_preview")}
|
||||||
|
subtitle={t("home.settings.other.skip_preview_description")}
|
||||||
|
disabled={pluginSettings?.skipPreview?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
groups={skipPreviewOptions}
|
||||||
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(
|
||||||
|
`home.settings.other.segment_skip_${settings.skipPreview}`,
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.skip_preview")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</ListGroup>
|
||||||
|
</DisabledSetting>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEGMENT_SKIP_OPTIONS = (
|
||||||
|
t: TFunction<"translation", undefined>,
|
||||||
|
): Array<{
|
||||||
|
label: string;
|
||||||
|
value: "none" | "ask" | "auto";
|
||||||
|
}> => [
|
||||||
|
{
|
||||||
|
label: t("home.settings.other.segment_skip_auto"),
|
||||||
|
value: "auto",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("home.settings.other.segment_skip_ask"),
|
||||||
|
value: "ask",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("home.settings.other.segment_skip_none"),
|
||||||
|
value: "none",
|
||||||
|
},
|
||||||
|
];
|
||||||
76
bun.lock
76
bun.lock
@@ -50,14 +50,14 @@
|
|||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-task-manager": "14.0.9",
|
"expo-task-manager": "14.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.10",
|
||||||
"i18next": "^25.0.0",
|
"i18next": "^26.0.0",
|
||||||
"jotai": "2.16.2",
|
"jotai": "2.16.2",
|
||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-i18next": "16.5.8",
|
"react-i18next": "17.0.8",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "1.1.0",
|
"react-native-bottom-tabs": "1.1.0",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.6",
|
"@babel/core": "7.28.6",
|
||||||
"@biomejs/biome": "2.3.11",
|
"@biomejs/biome": "2.3.11",
|
||||||
"@react-native-community/cli": "20.1.1",
|
"@react-native-community/cli": "20.1.3",
|
||||||
"@react-native-tvos/config-tv": "0.1.6",
|
"@react-native-tvos/config-tv": "0.1.6",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lodash": "4.17.23",
|
"@types/lodash": "4.17.23",
|
||||||
@@ -462,6 +462,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
@@ -512,29 +514,29 @@
|
|||||||
|
|
||||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||||
|
|
||||||
"@react-native-community/cli": ["@react-native-community/cli@20.1.1", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.1", "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-doctor": "20.1.1", "@react-native-community/cli-server-api": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "@react-native-community/cli-types": "20.1.1", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-aLPUx43+WSeTOaUepR2FBD5a1V0OAZ1QB2DOlRlW4fOEjtBXgv40eM/ho8g3WCvAOKfPvTvx4fZdcuovTyV81Q=="],
|
"@react-native-community/cli": ["@react-native-community/cli@20.1.3", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.3", "@react-native-community/cli-config": "20.1.3", "@react-native-community/cli-doctor": "20.1.3", "@react-native-community/cli-server-api": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "@react-native-community/cli-types": "20.1.3", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg=="],
|
||||||
|
|
||||||
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-6nGQ08w2+EcDwTFC4JFiW/wI2pLwzMrk9thz4um7tKRNW8sADX0IyCsfM2F4rHS720C0UNKYBZE9nAsfp8Vkcw=="],
|
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-sFLdLzapfC0scjgzBJJWYDY2RhHPjuuPkA5r6q0gc/UQH/izXpMpLrhh1DW84cMDraNACK0U62tU7ebNaQ1LMQ=="],
|
||||||
|
|
||||||
"@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-ajs2i56MANie/v0bMQ1BmRcrOb6MEvLT2rh/I1CA62NXGqF1Rxv6QwsN84LrADMXHRg8QiEMAIADkyDeQHt7Kg=="],
|
"@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-n73nW0cG92oNF0r994pPqm0DjAShOm3F8LSffDYhJqNAno+h/csmv/37iL4NtSpmKIO8xqsG3uVTXz9X/hzNaQ=="],
|
||||||
|
|
||||||
"@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-1iUV2rPAyoWPo8EceAFC2vZTF+pEd9YqS87c0aqpbGOFE0gs1rHEB+auVR8CdjzftR4U9sq6m2jrdst0rvpIkg=="],
|
"@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.6", "picocolors": "^1.1.1" } }, "sha512-DNHDP+OWLyhKShGciBqPcxhxfp1Z/7GQcb4F+TGyCeKQAr+JdnUjRXN3X+YCU/v+g2kbYYyRJKlGabzkVvdrAw=="],
|
||||||
|
|
||||||
"@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-doepJgLJVqeJb5tNoP9hyFIcoZ1OMGO7QN/YMuCCIjbThUQe/J87XdwPol3Qrjr58KRt9xeBVz+kHeW5mtSutw=="],
|
"@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-QX9B83nAfCPs0KiaYz61kAEHWr9sttooxzRzNdQwvZTwnsIpvWOT9GvMMj/19OeXiQzMJBzZX0Pgt6+spiUsDQ=="],
|
||||||
|
|
||||||
"@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.1", "", { "dependencies": { "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-platform-android": "20.1.1", "@react-native-community/cli-platform-apple": "20.1.1", "@react-native-community/cli-platform-ios": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-eFpg5wWnV7uGqvLemshpgj2trPD8cckqxBuI4nT7sxKF/YpA/e3nnnyytHxPP5EnYfWbMcqfaq8hDJoOnJinGQ=="],
|
"@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.3", "", { "dependencies": { "@react-native-community/cli-config": "20.1.3", "@react-native-community/cli-platform-android": "20.1.3", "@react-native-community/cli-platform-apple": "20.1.3", "@react-native-community/cli-platform-ios": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-EI+mAPWn255/WZ4CQohy1I049yiaxVr41C3BeQ2BCyhxODIDR8XRsLzYb1t9MfqK/C3ZncUN2mPSRXFeKPPI1w=="],
|
||||||
|
|
||||||
"@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-KPheizJQI0tVvBLy9owzpo+A9qDsDAa87e7a8xNaHnwqGpExnIzFPrbdvrltiZjstU2eB/+/UgNQxYIEd4Oc+g=="],
|
"@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.3", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-bzB9ELPOISuqgtDZXFPQlkuxx1YFkNx3cNgslc5ElCrk+5LeCLQLIBh/dmIuK8rwUrPcrramjeBj++Noc+TaAA=="],
|
||||||
|
|
||||||
"@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-mQEjOzRFCcQTrCt73Q/+5WWTfUg6U2vLZv5rPuFiNrLbrwRqxVH3OLaXg5gilJkDTJC80z8iOSsdd8MRxONOig=="],
|
"@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.3", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-xml-parser": "^5.3.6", "picocolors": "^1.1.1" } }, "sha512-XJ+DqAD4hkplWVXK5AMgN7pP9+4yRSe5KfZ/b42+ofkDBI55ALlUmX+9HWE3fMuRjcotTCoNZqX2ov97cFDXpQ=="],
|
||||||
|
|
||||||
"@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.1", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.1" } }, "sha512-6vr10/oSjKkZO/BBgfFJNQTC/0CDF4WrN8iW9ss+Kt6ZL2QrBXLYz7fobrrboOlHwqqs5EyQadlEaNii7gKRJg=="],
|
"@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.3", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.3" } }, "sha512-2qL48SINotuHbZO73cgqSwqd/OWNx0xTbFSdujhpogV4p8BNwYYypfjh4vJY5qJEB5PxuoVkMXT+aCADpg9nBg=="],
|
||||||
|
|
||||||
"@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-phHfiCa4WqfKfaoV2vGVR3ZrYQDQTpI1k+C+i6rXAxFGxPuy8IgFFVOSL543qjKPpHBVwLcA+/xAJCVpdyCtVQ=="],
|
"@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "body-parser": "^2.2.2", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-hsNsdUKZDd2T99OuNuiXz4VuvLa1UN0zcxefmPjXQgI0byrBLzzDr+o7p03sKuODSzKi2h+BMnUxiS07HACQLA=="],
|
||||||
|
|
||||||
"@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.1", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-j+zX/H2X+6ZGneIDj56tZ1Hbnip5nSfnq7yGlMyF/zm3U1hKp3G1jN5v0YEfnz/zEmjr7zruh4Y06KmZrF1lrA=="],
|
"@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.3", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-EAn0vPCMxtHhfWk2UwLmSUfPfLUnFgC7NjiVJVTKJyVk5qGnkPfoT8te/1IUXFTysUB0F0RIi+NgDB4usFOLeA=="],
|
||||||
|
|
||||||
"@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.1", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-Tp+s27I/RDONrGvWVj4IzEmga2HhJhXi8ZlZTfycMMyAcv4LG/CTPira+BUZs8nzLAJNrlJ79pVVPJPqQAe+aw=="],
|
"@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.3", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-IdAcegf0pH1hVraxWTG1ACLkYC0LDQfqtaEf42ESyLIF3Xap70JzL/9tAlxw7lSCPZPFWhrcgU0TBc4SkC/ecw=="],
|
||||||
|
|
||||||
"@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="],
|
"@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="],
|
||||||
|
|
||||||
@@ -774,7 +776,7 @@
|
|||||||
|
|
||||||
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
|
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||||
|
|
||||||
@@ -1094,7 +1096,9 @@
|
|||||||
|
|
||||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="],
|
"fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="],
|
||||||
|
|
||||||
|
"fast-xml-parser": ["fast-xml-parser@5.8.0", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", "strnum": "^2.3.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||||
|
|
||||||
@@ -1204,9 +1208,9 @@
|
|||||||
|
|
||||||
"hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
|
"hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
|
||||||
|
|
||||||
"i18next": ["i18next@25.6.1", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw=="],
|
"i18next": ["i18next@26.2.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
@@ -1400,7 +1404,7 @@
|
|||||||
|
|
||||||
"mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
|
"mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
|
||||||
|
|
||||||
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
||||||
|
|
||||||
@@ -1544,6 +1548,8 @@
|
|||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
|
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||||
|
|
||||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
@@ -1612,7 +1618,7 @@
|
|||||||
|
|
||||||
"qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="],
|
"qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="],
|
||||||
|
|
||||||
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
|
||||||
|
|
||||||
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
|
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
|
||||||
|
|
||||||
@@ -1622,7 +1628,7 @@
|
|||||||
|
|
||||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
@@ -1636,7 +1642,7 @@
|
|||||||
|
|
||||||
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
|
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
|
||||||
|
|
||||||
"react-i18next": ["react-i18next@16.5.8", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg=="],
|
"react-i18next": ["react-i18next@17.0.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5 || ^6" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw=="],
|
||||||
|
|
||||||
"react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
|
"react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
|
||||||
|
|
||||||
@@ -1868,7 +1874,7 @@
|
|||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
|
"strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||||
|
|
||||||
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
||||||
|
|
||||||
@@ -1932,7 +1938,7 @@
|
|||||||
|
|
||||||
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
||||||
|
|
||||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
@@ -2024,6 +2030,8 @@
|
|||||||
|
|
||||||
"xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="],
|
"xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="],
|
||||||
|
|
||||||
|
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
|
||||||
|
|
||||||
"xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="],
|
"xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="],
|
||||||
|
|
||||||
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||||
@@ -2206,8 +2214,6 @@
|
|||||||
|
|
||||||
"@react-native-community/cli-doctor/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"@react-native-community/cli-doctor/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
"@react-native-community/cli-doctor/yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
|
||||||
|
|
||||||
"@react-native-community/cli-server-api/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
|
"@react-native-community/cli-server-api/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
|
||||||
|
|
||||||
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
@@ -2282,8 +2288,6 @@
|
|||||||
|
|
||||||
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||||
|
|
||||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
|
||||||
|
|
||||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -2434,10 +2438,14 @@
|
|||||||
|
|
||||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||||
|
|
||||||
"react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
"react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||||
|
|
||||||
|
"react-i18next/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
|
|
||||||
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||||
|
|
||||||
"react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -2496,6 +2504,10 @@
|
|||||||
|
|
||||||
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||||
|
|
||||||
|
"type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
"whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
"whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
@@ -2846,8 +2858,6 @@
|
|||||||
|
|
||||||
"babel-preset-expo/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
"babel-preset-expo/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
|
||||||
|
|
||||||
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
@@ -3000,6 +3010,8 @@
|
|||||||
|
|
||||||
"patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
"patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
|
"raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
@@ -3026,6 +3038,8 @@
|
|||||||
|
|
||||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
|
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { BITRATES } from "@/components/BitrateSelector";
|
|||||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -15,6 +16,7 @@ import { ListGroup } from "../list/ListGroup";
|
|||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const PlaybackControlsSettings: React.FC = () => {
|
export const PlaybackControlsSettings: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
const { settings, updateSettings, pluginSettings } = useSettings();
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -248,6 +250,15 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
title={t("home.settings.other.max_auto_play_episode_count")}
|
title={t("home.settings.other.max_auto_play_episode_count")}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{/* Media Segment Skip Settings */}
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.segment_skip_settings")}
|
||||||
|
subtitle={t("home.settings.other.segment_skip_settings_description")}
|
||||||
|
onPress={() => router.push("/settings/segment-skip/page")}
|
||||||
|
>
|
||||||
|
<Ionicons name='chevron-forward' size={20} color='#8E8D91' />
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ interface BottomControlsProps {
|
|||||||
currentTime: number;
|
currentTime: number;
|
||||||
remainingTime: number;
|
remainingTime: number;
|
||||||
showSkipButton: boolean;
|
showSkipButton: boolean;
|
||||||
|
skipButtonText: string;
|
||||||
showSkipCreditButton: boolean;
|
showSkipCreditButton: boolean;
|
||||||
|
skipCreditButtonText: string;
|
||||||
hasContentAfterCredits: boolean;
|
hasContentAfterCredits: boolean;
|
||||||
skipIntro: () => void;
|
skipIntro: () => void;
|
||||||
skipCredit: () => void;
|
skipCredit: () => void;
|
||||||
@@ -67,7 +69,9 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
currentTime,
|
currentTime,
|
||||||
remainingTime,
|
remainingTime,
|
||||||
showSkipButton,
|
showSkipButton,
|
||||||
|
skipButtonText,
|
||||||
showSkipCreditButton,
|
showSkipCreditButton,
|
||||||
|
skipCreditButtonText,
|
||||||
hasContentAfterCredits,
|
hasContentAfterCredits,
|
||||||
skipIntro,
|
skipIntro,
|
||||||
skipCredit,
|
skipCredit,
|
||||||
@@ -136,7 +140,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
<SkipButton
|
<SkipButton
|
||||||
showButton={showSkipButton}
|
showButton={showSkipButton}
|
||||||
onPress={skipIntro}
|
onPress={skipIntro}
|
||||||
buttonText='Skip Intro'
|
buttonText={skipButtonText}
|
||||||
/>
|
/>
|
||||||
{/* Smart Skip Credits behavior:
|
{/* Smart Skip Credits behavior:
|
||||||
- Show "Skip Credits" if there's content after credits OR no next episode
|
- Show "Skip Credits" if there's content after credits OR no next episode
|
||||||
@@ -146,7 +150,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
showSkipCreditButton && (hasContentAfterCredits || !nextItem)
|
showSkipCreditButton && (hasContentAfterCredits || !nextItem)
|
||||||
}
|
}
|
||||||
onPress={skipCredit}
|
onPress={skipCredit}
|
||||||
buttonText='Skip Credits'
|
buttonText={skipCreditButtonText}
|
||||||
/>
|
/>
|
||||||
{settings.autoPlayNextEpisode !== false &&
|
{settings.autoPlayNextEpisode !== false &&
|
||||||
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ import type {
|
|||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { type FC, useCallback, useEffect, useState } from "react";
|
import {
|
||||||
|
type FC,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { StyleSheet, useWindowDimensions, View } from "react-native";
|
import { StyleSheet, useWindowDimensions, View } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
@@ -16,17 +24,17 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
|
||||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||||
|
import { useSegmentSkipper } from "@/hooks/useSegmentSkipper";
|
||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||||
import type { TechnicalInfo } from "@/modules/mpv-player";
|
import type { TechnicalInfo } from "@/modules/mpv-player";
|
||||||
import { DownloadedItem } from "@/providers/Downloads/types";
|
import { DownloadedItem } from "@/providers/Downloads/types";
|
||||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
import { ticksToMs } from "@/utils/time";
|
import { useSegments } from "@/utils/segments";
|
||||||
|
import { msToSeconds, ticksToMs } from "@/utils/time";
|
||||||
import { BottomControls } from "./BottomControls";
|
import { BottomControls } from "./BottomControls";
|
||||||
import { CenterControls } from "./CenterControls";
|
import { CenterControls } from "./CenterControls";
|
||||||
import { CONTROLS_CONSTANTS } from "./constants";
|
import { CONTROLS_CONSTANTS } from "./constants";
|
||||||
@@ -42,6 +50,9 @@ import { useControlsTimeout } from "./useControlsTimeout";
|
|||||||
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
||||||
import { type AspectRatio } from "./VideoScalingModeSelector";
|
import { type AspectRatio } from "./VideoScalingModeSelector";
|
||||||
|
|
||||||
|
// No-op function to avoid creating new references on every render
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
@@ -110,6 +121,24 @@ export const Controls: FC<Props> = ({
|
|||||||
const [episodeView, setEpisodeView] = useState(false);
|
const [episodeView, setEpisodeView] = useState(false);
|
||||||
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
||||||
|
|
||||||
|
// Ref to track pending play timeout for cleanup and cancellation
|
||||||
|
const playTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
// Mutable ref tracking isPlaying to avoid stale closures in seekMs timeout
|
||||||
|
const playingRef = useRef(isPlaying);
|
||||||
|
useEffect(() => {
|
||||||
|
playingRef.current = isPlaying;
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
|
// Clean up timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (playTimeoutRef.current) {
|
||||||
|
clearTimeout(playTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
||||||
const { previousItem, nextItem } = usePlaybackManager({
|
const { previousItem, nextItem } = usePlaybackManager({
|
||||||
item,
|
item,
|
||||||
@@ -300,27 +329,125 @@ export const Controls: FC<Props> = ({
|
|||||||
subtitleIndex: string;
|
subtitleIndex: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { showSkipButton, skipIntro } = useIntroSkipper(
|
// Fetch all segments for the current item
|
||||||
item.Id!,
|
const { data: segments } = useSegments(
|
||||||
currentTime,
|
item.Id ?? "",
|
||||||
seek,
|
|
||||||
play,
|
|
||||||
offline,
|
offline,
|
||||||
api,
|
|
||||||
downloadedFiles,
|
downloadedFiles,
|
||||||
|
api,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { showSkipCreditButton, skipCredit, hasContentAfterCredits } =
|
// Convert milliseconds to seconds for segment comparison
|
||||||
useCreditSkipper(
|
const currentTimeSeconds = msToSeconds(currentTime);
|
||||||
item.Id!,
|
const maxSeconds = maxMs ? msToSeconds(maxMs) : undefined;
|
||||||
currentTime,
|
|
||||||
seek,
|
// Wrapper to convert segment skip from seconds to milliseconds
|
||||||
play,
|
// Includes 200ms delay to allow seek operation to complete before resuming playback
|
||||||
offline,
|
const seekMs = useCallback(
|
||||||
api,
|
(timeInSeconds: number) => {
|
||||||
downloadedFiles,
|
// Cancel any pending play call to avoid race conditions
|
||||||
maxMs,
|
if (playTimeoutRef.current) {
|
||||||
);
|
clearTimeout(playTimeoutRef.current);
|
||||||
|
}
|
||||||
|
seek(timeInSeconds * 1000);
|
||||||
|
// Brief delay ensures the seek operation completes before resuming playback
|
||||||
|
// Without this, playback may resume from the old position
|
||||||
|
// Read latest isPlaying from ref to avoid stale closure
|
||||||
|
playTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (playingRef.current) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
playTimeoutRef.current = null;
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
[seek, play],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use unified segment skipper for all segment types
|
||||||
|
const introSkipper = useSegmentSkipper({
|
||||||
|
segments: segments?.introSegments || [],
|
||||||
|
segmentType: "Intro",
|
||||||
|
currentTime: currentTimeSeconds,
|
||||||
|
seek: seekMs,
|
||||||
|
isPaused: !isPlaying,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outroSkipper = useSegmentSkipper({
|
||||||
|
segments: segments?.creditSegments || [],
|
||||||
|
segmentType: "Outro",
|
||||||
|
currentTime: currentTimeSeconds,
|
||||||
|
totalDuration: maxSeconds,
|
||||||
|
seek: seekMs,
|
||||||
|
isPaused: !isPlaying,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recapSkipper = useSegmentSkipper({
|
||||||
|
segments: segments?.recapSegments || [],
|
||||||
|
segmentType: "Recap",
|
||||||
|
currentTime: currentTimeSeconds,
|
||||||
|
seek: seekMs,
|
||||||
|
isPaused: !isPlaying,
|
||||||
|
});
|
||||||
|
|
||||||
|
const commercialSkipper = useSegmentSkipper({
|
||||||
|
segments: segments?.commercialSegments || [],
|
||||||
|
segmentType: "Commercial",
|
||||||
|
currentTime: currentTimeSeconds,
|
||||||
|
seek: seekMs,
|
||||||
|
isPaused: !isPlaying,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewSkipper = useSegmentSkipper({
|
||||||
|
segments: segments?.previewSegments || [],
|
||||||
|
segmentType: "Preview",
|
||||||
|
currentTime: currentTimeSeconds,
|
||||||
|
seek: seekMs,
|
||||||
|
isPaused: !isPlaying,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine which segment button to show (priority order)
|
||||||
|
// Commercial > Recap > Intro > Preview > Outro
|
||||||
|
const activeSegment = useMemo(() => {
|
||||||
|
if (commercialSkipper.currentSegment)
|
||||||
|
return { type: "Commercial", ...commercialSkipper };
|
||||||
|
if (recapSkipper.currentSegment) return { type: "Recap", ...recapSkipper };
|
||||||
|
if (introSkipper.currentSegment) return { type: "Intro", ...introSkipper };
|
||||||
|
if (previewSkipper.currentSegment)
|
||||||
|
return { type: "Preview", ...previewSkipper };
|
||||||
|
if (outroSkipper.currentSegment) return { type: "Outro", ...outroSkipper };
|
||||||
|
return null;
|
||||||
|
}, [
|
||||||
|
commercialSkipper.currentSegment,
|
||||||
|
recapSkipper.currentSegment,
|
||||||
|
introSkipper.currentSegment,
|
||||||
|
previewSkipper.currentSegment,
|
||||||
|
outroSkipper.currentSegment,
|
||||||
|
commercialSkipper,
|
||||||
|
recapSkipper,
|
||||||
|
introSkipper,
|
||||||
|
previewSkipper,
|
||||||
|
outroSkipper,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Legacy compatibility: map to old variable names
|
||||||
|
const showSkipButton = !!(
|
||||||
|
activeSegment &&
|
||||||
|
["Intro", "Recap", "Commercial", "Preview"].includes(activeSegment.type)
|
||||||
|
);
|
||||||
|
const skipIntro = activeSegment?.skipSegment || noop;
|
||||||
|
const showSkipCreditButton = activeSegment?.type === "Outro";
|
||||||
|
const skipCredit = outroSkipper.skipSegment || noop;
|
||||||
|
const hasContentAfterCredits =
|
||||||
|
outroSkipper.currentSegment && maxSeconds
|
||||||
|
? outroSkipper.currentSegment.endTime < maxSeconds
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Get button text based on segment type using i18n
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const skipButtonText = activeSegment
|
||||||
|
? t(`player.skip_${activeSegment.type.toLowerCase()}`)
|
||||||
|
: t("player.skip_intro");
|
||||||
|
const skipCreditButtonText = t("player.skip_outro");
|
||||||
|
|
||||||
const goToItemCommon = useCallback(
|
const goToItemCommon = useCallback(
|
||||||
(item: BaseItemDto) => {
|
(item: BaseItemDto) => {
|
||||||
@@ -534,7 +661,9 @@ export const Controls: FC<Props> = ({
|
|||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
remainingTime={remainingTime}
|
remainingTime={remainingTime}
|
||||||
showSkipButton={showSkipButton}
|
showSkipButton={showSkipButton}
|
||||||
|
skipButtonText={skipButtonText}
|
||||||
showSkipCreditButton={showSkipCreditButton}
|
showSkipCreditButton={showSkipCreditButton}
|
||||||
|
skipCreditButtonText={skipCreditButtonText}
|
||||||
hasContentAfterCredits={hasContentAfterCredits}
|
hasContentAfterCredits={hasContentAfterCredits}
|
||||||
skipIntro={skipIntro}
|
skipIntro={skipIntro}
|
||||||
skipCredit={skipCredit}
|
skipCredit={skipCredit}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { DownloadedItem } from "@/providers/Downloads/types";
|
|
||||||
import { useSegments } from "@/utils/segments";
|
|
||||||
import { msToSeconds, secondsToMs } from "@/utils/time";
|
|
||||||
import { useHaptic } from "./useHaptic";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook to handle skipping credits in a media player.
|
|
||||||
* The player reports time values in milliseconds.
|
|
||||||
*/
|
|
||||||
export const useCreditSkipper = (
|
|
||||||
itemId: string,
|
|
||||||
currentTime: number,
|
|
||||||
seek: (ms: number) => void,
|
|
||||||
play: () => void,
|
|
||||||
isOffline = false,
|
|
||||||
api: Api | null = null,
|
|
||||||
downloadedFiles: DownloadedItem[] | undefined = undefined,
|
|
||||||
totalDuration?: number,
|
|
||||||
) => {
|
|
||||||
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
// Convert ms to seconds for comparison with timestamps
|
|
||||||
const currentTimeSeconds = msToSeconds(currentTime);
|
|
||||||
|
|
||||||
const totalDurationInSeconds =
|
|
||||||
totalDuration != null ? msToSeconds(totalDuration) : undefined;
|
|
||||||
|
|
||||||
// Regular function (not useCallback) to match useIntroSkipper pattern
|
|
||||||
const wrappedSeek = (seconds: number) => {
|
|
||||||
seek(secondsToMs(seconds));
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: segments } = useSegments(
|
|
||||||
itemId,
|
|
||||||
isOffline,
|
|
||||||
downloadedFiles,
|
|
||||||
api,
|
|
||||||
);
|
|
||||||
const creditTimestamps = segments?.creditSegments?.[0];
|
|
||||||
|
|
||||||
// Determine if there's content after credits (credits don't extend to video end)
|
|
||||||
// Use a 5-second buffer to account for timing discrepancies
|
|
||||||
const hasContentAfterCredits = (() => {
|
|
||||||
if (
|
|
||||||
!creditTimestamps ||
|
|
||||||
totalDurationInSeconds == null ||
|
|
||||||
!Number.isFinite(totalDurationInSeconds)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const creditsEndToVideoEnd =
|
|
||||||
totalDurationInSeconds - creditTimestamps.endTime;
|
|
||||||
// If credits end more than 5 seconds before video ends, there's content after
|
|
||||||
return creditsEndToVideoEnd > 5;
|
|
||||||
})();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (creditTimestamps) {
|
|
||||||
const shouldShow =
|
|
||||||
currentTimeSeconds > creditTimestamps.startTime &&
|
|
||||||
currentTimeSeconds < creditTimestamps.endTime;
|
|
||||||
|
|
||||||
setShowSkipCreditButton(shouldShow);
|
|
||||||
} else {
|
|
||||||
// Reset button state when no credit timestamps exist
|
|
||||||
if (showSkipCreditButton) {
|
|
||||||
setShowSkipCreditButton(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [creditTimestamps, currentTimeSeconds, showSkipCreditButton]);
|
|
||||||
|
|
||||||
const skipCredit = useCallback(() => {
|
|
||||||
if (!creditTimestamps) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
lightHapticFeedback();
|
|
||||||
|
|
||||||
// Calculate the target seek position
|
|
||||||
let seekTarget = creditTimestamps.endTime;
|
|
||||||
|
|
||||||
// If we have total duration, ensure we don't seek past the end of the video.
|
|
||||||
// Some media sources report credit end times that exceed the actual video duration,
|
|
||||||
// which causes the player to pause/stop when seeking past the end.
|
|
||||||
// Leave a small buffer (2 seconds) to trigger the natural end-of-video flow
|
|
||||||
// (next episode countdown, etc.) instead of an abrupt pause.
|
|
||||||
if (totalDurationInSeconds && seekTarget >= totalDurationInSeconds) {
|
|
||||||
seekTarget = Math.max(0, totalDurationInSeconds - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
wrappedSeek(seekTarget);
|
|
||||||
setTimeout(() => {
|
|
||||||
play();
|
|
||||||
}, 200);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[CREDIT_SKIPPER] Error skipping credit", error);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
creditTimestamps,
|
|
||||||
lightHapticFeedback,
|
|
||||||
wrappedSeek,
|
|
||||||
play,
|
|
||||||
totalDurationInSeconds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { showSkipCreditButton, skipCredit, hasContentAfterCredits };
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { DownloadedItem } from "@/providers/Downloads/types";
|
|
||||||
import { useSegments } from "@/utils/segments";
|
|
||||||
import { msToSeconds, secondsToMs } from "@/utils/time";
|
|
||||||
import { useHaptic } from "./useHaptic";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook to handle skipping intros in a media player.
|
|
||||||
* MPV player uses milliseconds for time.
|
|
||||||
*
|
|
||||||
* @param {number} currentTime - The current playback time in milliseconds.
|
|
||||||
*/
|
|
||||||
export const useIntroSkipper = (
|
|
||||||
itemId: string,
|
|
||||||
currentTime: number,
|
|
||||||
seek: (ms: number) => void,
|
|
||||||
play: () => void,
|
|
||||||
isOffline = false,
|
|
||||||
api: Api | null = null,
|
|
||||||
downloadedFiles: DownloadedItem[] | undefined = undefined,
|
|
||||||
) => {
|
|
||||||
const [showSkipButton, setShowSkipButton] = useState(false);
|
|
||||||
// Convert ms to seconds for comparison with timestamps
|
|
||||||
const currentTimeSeconds = msToSeconds(currentTime);
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const wrappedSeek = (seconds: number) => {
|
|
||||||
seek(secondsToMs(seconds));
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: segments } = useSegments(
|
|
||||||
itemId,
|
|
||||||
isOffline,
|
|
||||||
downloadedFiles,
|
|
||||||
api,
|
|
||||||
);
|
|
||||||
const introTimestamps = segments?.introSegments?.[0];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (introTimestamps) {
|
|
||||||
const shouldShow =
|
|
||||||
currentTimeSeconds > introTimestamps.startTime &&
|
|
||||||
currentTimeSeconds < introTimestamps.endTime;
|
|
||||||
|
|
||||||
setShowSkipButton(shouldShow);
|
|
||||||
} else {
|
|
||||||
if (showSkipButton) {
|
|
||||||
setShowSkipButton(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [introTimestamps, currentTimeSeconds, showSkipButton]);
|
|
||||||
|
|
||||||
const skipIntro = useCallback(() => {
|
|
||||||
if (!introTimestamps) return;
|
|
||||||
try {
|
|
||||||
lightHapticFeedback();
|
|
||||||
wrappedSeek(introTimestamps.endTime);
|
|
||||||
setTimeout(() => {
|
|
||||||
play();
|
|
||||||
}, 200);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[INTRO_SKIPPER] Error skipping intro", error);
|
|
||||||
}
|
|
||||||
}, [introTimestamps, lightHapticFeedback, wrappedSeek, play]);
|
|
||||||
|
|
||||||
return { showSkipButton, skipIntro };
|
|
||||||
};
|
|
||||||
113
hooks/useSegmentSkipper.ts
Normal file
113
hooks/useSegmentSkipper.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { MediaTimeSegment } from "@/providers/Downloads/types";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { useHaptic } from "./useHaptic";
|
||||||
|
|
||||||
|
type SegmentType = "Intro" | "Outro" | "Recap" | "Commercial" | "Preview";
|
||||||
|
|
||||||
|
interface UseSegmentSkipperProps {
|
||||||
|
segments: MediaTimeSegment[];
|
||||||
|
segmentType: SegmentType;
|
||||||
|
currentTime: number;
|
||||||
|
totalDuration?: number;
|
||||||
|
seek: (time: number) => void;
|
||||||
|
isPaused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseSegmentSkipperReturn {
|
||||||
|
currentSegment: MediaTimeSegment | null;
|
||||||
|
skipSegment: (notifyOrUseHaptics?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic hook to handle all media segment types (intro, outro, recap, commercial, preview)
|
||||||
|
* Supports three modes: 'none' (disabled), 'ask' (show button), 'auto' (auto-skip)
|
||||||
|
*/
|
||||||
|
export const useSegmentSkipper = ({
|
||||||
|
segments,
|
||||||
|
segmentType,
|
||||||
|
currentTime,
|
||||||
|
totalDuration,
|
||||||
|
seek,
|
||||||
|
isPaused,
|
||||||
|
}: UseSegmentSkipperProps): UseSegmentSkipperReturn => {
|
||||||
|
const { settings } = useSettings();
|
||||||
|
const haptic = useHaptic();
|
||||||
|
const autoSkipTriggeredRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// Get skip mode based on segment type
|
||||||
|
const skipMode = (() => {
|
||||||
|
switch (segmentType) {
|
||||||
|
case "Intro":
|
||||||
|
return settings.skipIntro;
|
||||||
|
case "Outro":
|
||||||
|
return settings.skipOutro;
|
||||||
|
case "Recap":
|
||||||
|
return settings.skipRecap;
|
||||||
|
case "Commercial":
|
||||||
|
return settings.skipCommercial;
|
||||||
|
case "Preview":
|
||||||
|
return settings.skipPreview;
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Find current segment
|
||||||
|
const currentSegment =
|
||||||
|
segments.find(
|
||||||
|
(segment) =>
|
||||||
|
currentTime >= segment.startTime && currentTime < segment.endTime,
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
// Skip function with optional haptic feedback
|
||||||
|
const skipSegment = useCallback(
|
||||||
|
(notifyOrUseHaptics = true) => {
|
||||||
|
if (!currentSegment || skipMode === "none") return;
|
||||||
|
|
||||||
|
// For Outro segments, prevent seeking past the end
|
||||||
|
if (
|
||||||
|
segmentType === "Outro" &&
|
||||||
|
totalDuration != null &&
|
||||||
|
Number.isFinite(totalDuration)
|
||||||
|
) {
|
||||||
|
const seekTime = Math.min(currentSegment.endTime, totalDuration);
|
||||||
|
seek(seekTime);
|
||||||
|
} else {
|
||||||
|
seek(currentSegment.endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trigger haptic feedback if explicitly requested (manual skip)
|
||||||
|
if (notifyOrUseHaptics) {
|
||||||
|
haptic();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentSegment, segmentType, totalDuration, seek, haptic, skipMode],
|
||||||
|
);
|
||||||
|
// Auto-skip logic when mode is 'auto'
|
||||||
|
useEffect(() => {
|
||||||
|
if (skipMode !== "auto" || isPaused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track segment identity to avoid re-triggering on pause/unpause
|
||||||
|
const segmentId = currentSegment
|
||||||
|
? `${currentSegment.startTime}-${currentSegment.endTime}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (currentSegment && autoSkipTriggeredRef.current !== segmentId) {
|
||||||
|
autoSkipTriggeredRef.current = segmentId;
|
||||||
|
skipSegment(false); // Don't trigger haptics for auto-skip
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSegment) {
|
||||||
|
autoSkipTriggeredRef.current = null;
|
||||||
|
}
|
||||||
|
}, [currentSegment, skipMode, isPaused, skipSegment]);
|
||||||
|
|
||||||
|
// Return null segment if skip mode is 'none'
|
||||||
|
return {
|
||||||
|
currentSegment: skipMode === "none" ? null : currentSegment,
|
||||||
|
skipSegment,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"android:tv": "cross-env EXPO_TV=1 expo run:android",
|
"android:tv": "cross-env EXPO_TV=1 expo run:android",
|
||||||
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
|
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
|
||||||
"ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production",
|
"ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production",
|
||||||
|
"ios:unsigned-build:tv": "cross-env EXPO_TV=1 bun scripts/ios/build-ios.ts --production",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"typecheck": "node scripts/typecheck.js",
|
"typecheck": "node scripts/typecheck.js",
|
||||||
"check": "biome check . --max-diagnostics 1000",
|
"check": "biome check . --max-diagnostics 1000",
|
||||||
@@ -70,14 +71,14 @@
|
|||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-task-manager": "14.0.9",
|
"expo-task-manager": "14.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.10",
|
||||||
"i18next": "^25.0.0",
|
"i18next": "^26.0.0",
|
||||||
"jotai": "2.16.2",
|
"jotai": "2.16.2",
|
||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-i18next": "16.5.8",
|
"react-i18next": "17.0.8",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "1.1.0",
|
"react-native-bottom-tabs": "1.1.0",
|
||||||
@@ -117,7 +118,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.6",
|
"@babel/core": "7.28.6",
|
||||||
"@biomejs/biome": "2.3.11",
|
"@biomejs/biome": "2.3.11",
|
||||||
"@react-native-community/cli": "20.1.1",
|
"@react-native-community/cli": "20.1.3",
|
||||||
"@react-native-tvos/config-tv": "0.1.6",
|
"@react-native-tvos/config-tv": "0.1.6",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lodash": "4.17.23",
|
"@types/lodash": "4.17.23",
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ export interface MediaTimeSegment {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Segment {
|
|
||||||
startTime: number;
|
|
||||||
endTime: number;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Represents a single downloaded media item with all necessary metadata for offline playback. */
|
/** Represents a single downloaded media item with all necessary metadata for offline playback. */
|
||||||
export interface DownloadedItem {
|
export interface DownloadedItem {
|
||||||
/** The Jellyfin item DTO. */
|
/** The Jellyfin item DTO. */
|
||||||
@@ -56,6 +50,12 @@ export interface DownloadedItem {
|
|||||||
introSegments?: MediaTimeSegment[];
|
introSegments?: MediaTimeSegment[];
|
||||||
/** The credit segments for the item. */
|
/** The credit segments for the item. */
|
||||||
creditSegments?: MediaTimeSegment[];
|
creditSegments?: MediaTimeSegment[];
|
||||||
|
/** The recap segments for the item. */
|
||||||
|
recapSegments?: MediaTimeSegment[];
|
||||||
|
/** The commercial segments for the item. */
|
||||||
|
commercialSegments?: MediaTimeSegment[];
|
||||||
|
/** The preview segments for the item. */
|
||||||
|
previewSegments?: MediaTimeSegment[];
|
||||||
/** The user data for the item. */
|
/** The user data for the item. */
|
||||||
userData: UserData;
|
userData: UserData;
|
||||||
}
|
}
|
||||||
@@ -144,6 +144,12 @@ export type JobStatus = {
|
|||||||
introSegments?: MediaTimeSegment[];
|
introSegments?: MediaTimeSegment[];
|
||||||
/** Pre-downloaded credit segments (optional) - downloaded before video starts */
|
/** Pre-downloaded credit segments (optional) - downloaded before video starts */
|
||||||
creditSegments?: MediaTimeSegment[];
|
creditSegments?: MediaTimeSegment[];
|
||||||
|
/** Pre-downloaded recap segments (optional) - downloaded before video starts */
|
||||||
|
recapSegments?: MediaTimeSegment[];
|
||||||
|
/** Pre-downloaded commercial segments (optional) - downloaded before video starts */
|
||||||
|
commercialSegments?: MediaTimeSegment[];
|
||||||
|
/** Pre-downloaded preview segments (optional) - downloaded before video starts */
|
||||||
|
previewSegments?: MediaTimeSegment[];
|
||||||
/** The audio stream index selected for this download */
|
/** The audio stream index selected for this download */
|
||||||
audioStreamIndex?: number;
|
audioStreamIndex?: number;
|
||||||
/** The subtitle stream index selected for this download */
|
/** The subtitle stream index selected for this download */
|
||||||
|
|||||||
@@ -24,6 +24,31 @@
|
|||||||
"too_old_server_text": "Unsupported Jellyfin Server Discovered",
|
"too_old_server_text": "Unsupported Jellyfin Server Discovered",
|
||||||
"too_old_server_description": "Please update Jellyfin to the latest version"
|
"too_old_server_description": "Please update Jellyfin to the latest version"
|
||||||
},
|
},
|
||||||
|
"player": {
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_outro": "Skip Outro",
|
||||||
|
"skip_recap": "Skip Recap",
|
||||||
|
"skip_commercial": "Skip Commercial",
|
||||||
|
"skip_preview": "Skip Preview",
|
||||||
|
"error": "Error",
|
||||||
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
|
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
|
"client_error": "Client Error",
|
||||||
|
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
|
||||||
|
"message_from_server": "Message from Server: {{message}}",
|
||||||
|
"next_episode": "Next Episode",
|
||||||
|
"refresh_tracks": "Refresh Tracks",
|
||||||
|
"audio_tracks": "Audio Tracks:",
|
||||||
|
"playback_state": "Playback State:",
|
||||||
|
"index": "Index:",
|
||||||
|
"continue_watching": "Continue Watching",
|
||||||
|
"go_back": "Go Back",
|
||||||
|
"downloaded_file_title": "You have this file downloaded",
|
||||||
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
|
"downloaded_file_yes": "Yes",
|
||||||
|
"downloaded_file_no": "No",
|
||||||
|
"downloaded_file_cancel": "Cancel"
|
||||||
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
@@ -308,6 +333,21 @@
|
|||||||
"default_playback_speed": "Default Playback Speed",
|
"default_playback_speed": "Default Playback Speed",
|
||||||
"auto_play_next_episode": "Auto-play Next Episode",
|
"auto_play_next_episode": "Auto-play Next Episode",
|
||||||
"max_auto_play_episode_count": "Max Auto Play Episode Count",
|
"max_auto_play_episode_count": "Max Auto Play Episode Count",
|
||||||
|
"segment_skip_settings": "Segment Skip Settings",
|
||||||
|
"segment_skip_settings_description": "Configure skip behavior for intros, credits, and other segments",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_intro_description": "Action when intro segment is detected",
|
||||||
|
"skip_outro": "Skip Outro/Credits",
|
||||||
|
"skip_outro_description": "Action when outro/credits segment is detected",
|
||||||
|
"skip_recap": "Skip Recap",
|
||||||
|
"skip_recap_description": "Action when recap segment is detected",
|
||||||
|
"skip_commercial": "Skip Commercial",
|
||||||
|
"skip_commercial_description": "Action when commercial segment is detected",
|
||||||
|
"skip_preview": "Skip Preview",
|
||||||
|
"skip_preview_description": "Action when preview segment is detected",
|
||||||
|
"segment_skip_none": "None",
|
||||||
|
"segment_skip_ask": "Show Skip Button",
|
||||||
|
"segment_skip_auto": "Auto Skip",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@@ -590,26 +630,6 @@
|
|||||||
"custom_links": {
|
"custom_links": {
|
||||||
"no_links": "No Links"
|
"no_links": "No Links"
|
||||||
},
|
},
|
||||||
"player": {
|
|
||||||
"error": "Error",
|
|
||||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
|
||||||
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
|
||||||
"client_error": "Client Error",
|
|
||||||
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
|
|
||||||
"message_from_server": "Message from Server: {{message}}",
|
|
||||||
"next_episode": "Next Episode",
|
|
||||||
"refresh_tracks": "Refresh Tracks",
|
|
||||||
"audio_tracks": "Audio Tracks:",
|
|
||||||
"playback_state": "Playback State:",
|
|
||||||
"index": "Index:",
|
|
||||||
"continue_watching": "Continue Watching",
|
|
||||||
"go_back": "Go Back",
|
|
||||||
"downloaded_file_title": "You have this file downloaded",
|
|
||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
|
||||||
"downloaded_file_yes": "Yes",
|
|
||||||
"downloaded_file_no": "No",
|
|
||||||
"downloaded_file_cancel": "Cancel"
|
|
||||||
},
|
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
"no_items_to_display": "No Items to Display",
|
"no_items_to_display": "No Items to Display",
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ export enum VideoPlayer {
|
|||||||
MPV = 0,
|
MPV = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Segment skip behavior options
|
||||||
|
export type SegmentSkipMode = "none" | "ask" | "auto";
|
||||||
|
|
||||||
// Audio transcoding mode - controls how surround audio is handled
|
// Audio transcoding mode - controls how surround audio is handled
|
||||||
// This controls server-side transcoding behavior for audio streams.
|
// This controls server-side transcoding behavior for audio streams.
|
||||||
// MPV decodes via FFmpeg and supports most formats, but mobile devices
|
// MPV decodes via FFmpeg and supports most formats, but mobile devices
|
||||||
@@ -181,6 +184,12 @@ export type Settings = {
|
|||||||
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
|
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
|
||||||
autoPlayEpisodeCount: number;
|
autoPlayEpisodeCount: number;
|
||||||
autoPlayNextEpisode: boolean;
|
autoPlayNextEpisode: boolean;
|
||||||
|
// Media segment skip preferences
|
||||||
|
skipIntro: SegmentSkipMode;
|
||||||
|
skipOutro: SegmentSkipMode;
|
||||||
|
skipRecap: SegmentSkipMode;
|
||||||
|
skipCommercial: SegmentSkipMode;
|
||||||
|
skipPreview: SegmentSkipMode;
|
||||||
// Playback speed settings
|
// Playback speed settings
|
||||||
defaultPlaybackSpeed: number;
|
defaultPlaybackSpeed: number;
|
||||||
playbackSpeedPerMedia: Record<string, number>;
|
playbackSpeedPerMedia: Record<string, number>;
|
||||||
@@ -266,6 +275,12 @@ export const defaultValues: Settings = {
|
|||||||
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
|
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
|
||||||
autoPlayEpisodeCount: 0,
|
autoPlayEpisodeCount: 0,
|
||||||
autoPlayNextEpisode: true,
|
autoPlayNextEpisode: true,
|
||||||
|
// Media segment skip defaults
|
||||||
|
skipIntro: "ask",
|
||||||
|
skipOutro: "ask",
|
||||||
|
skipRecap: "ask",
|
||||||
|
skipCommercial: "ask",
|
||||||
|
skipPreview: "ask",
|
||||||
// Playback speed defaults
|
// Playback speed defaults
|
||||||
defaultPlaybackSpeed: 1.0,
|
defaultPlaybackSpeed: 1.0,
|
||||||
playbackSpeedPerMedia: {},
|
playbackSpeedPerMedia: {},
|
||||||
|
|||||||
@@ -74,10 +74,16 @@ export const getSegmentsForItem = (
|
|||||||
): {
|
): {
|
||||||
introSegments: MediaTimeSegment[];
|
introSegments: MediaTimeSegment[];
|
||||||
creditSegments: MediaTimeSegment[];
|
creditSegments: MediaTimeSegment[];
|
||||||
|
recapSegments: MediaTimeSegment[];
|
||||||
|
commercialSegments: MediaTimeSegment[];
|
||||||
|
previewSegments: MediaTimeSegment[];
|
||||||
} => {
|
} => {
|
||||||
return {
|
return {
|
||||||
introSegments: item.introSegments || [],
|
introSegments: item.introSegments || [],
|
||||||
creditSegments: item.creditSegments || [],
|
creditSegments: item.creditSegments || [],
|
||||||
|
recapSegments: item.recapSegments || [],
|
||||||
|
commercialSegments: item.commercialSegments || [],
|
||||||
|
previewSegments: item.previewSegments || [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,6 +101,9 @@ const fetchMediaSegments = async (
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
introSegments: MediaTimeSegment[];
|
introSegments: MediaTimeSegment[];
|
||||||
creditSegments: MediaTimeSegment[];
|
creditSegments: MediaTimeSegment[];
|
||||||
|
recapSegments: MediaTimeSegment[];
|
||||||
|
commercialSegments: MediaTimeSegment[];
|
||||||
|
previewSegments: MediaTimeSegment[];
|
||||||
} | null> => {
|
} | null> => {
|
||||||
try {
|
try {
|
||||||
const response = await api.axiosInstance.get<MediaSegmentsResponse>(
|
const response = await api.axiosInstance.get<MediaSegmentsResponse>(
|
||||||
@@ -102,13 +111,22 @@ const fetchMediaSegments = async (
|
|||||||
{
|
{
|
||||||
headers: getAuthHeaders(api),
|
headers: getAuthHeaders(api),
|
||||||
params: {
|
params: {
|
||||||
includeSegmentTypes: ["Intro", "Outro"],
|
includeSegmentTypes: [
|
||||||
|
"Intro",
|
||||||
|
"Outro",
|
||||||
|
"Recap",
|
||||||
|
"Commercial",
|
||||||
|
"Preview",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const introSegments: MediaTimeSegment[] = [];
|
const introSegments: MediaTimeSegment[] = [];
|
||||||
const creditSegments: MediaTimeSegment[] = [];
|
const creditSegments: MediaTimeSegment[] = [];
|
||||||
|
const recapSegments: MediaTimeSegment[] = [];
|
||||||
|
const commercialSegments: MediaTimeSegment[] = [];
|
||||||
|
const previewSegments: MediaTimeSegment[] = [];
|
||||||
|
|
||||||
response.data.Items.forEach((segment) => {
|
response.data.Items.forEach((segment) => {
|
||||||
const timeSegment: MediaTimeSegment = {
|
const timeSegment: MediaTimeSegment = {
|
||||||
@@ -124,13 +142,27 @@ const fetchMediaSegments = async (
|
|||||||
case "Outro":
|
case "Outro":
|
||||||
creditSegments.push(timeSegment);
|
creditSegments.push(timeSegment);
|
||||||
break;
|
break;
|
||||||
// Optionally handle other types like Recap, Commercial, Preview
|
case "Recap":
|
||||||
|
recapSegments.push(timeSegment);
|
||||||
|
break;
|
||||||
|
case "Commercial":
|
||||||
|
commercialSegments.push(timeSegment);
|
||||||
|
break;
|
||||||
|
case "Preview":
|
||||||
|
previewSegments.push(timeSegment);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { introSegments, creditSegments };
|
return {
|
||||||
|
introSegments,
|
||||||
|
creditSegments,
|
||||||
|
recapSegments,
|
||||||
|
commercialSegments,
|
||||||
|
previewSegments,
|
||||||
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Return null to indicate we should try legacy endpoints
|
// Return null to indicate we should try legacy endpoints
|
||||||
return null;
|
return null;
|
||||||
@@ -146,45 +178,47 @@ const fetchLegacySegments = async (
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
introSegments: MediaTimeSegment[];
|
introSegments: MediaTimeSegment[];
|
||||||
creditSegments: MediaTimeSegment[];
|
creditSegments: MediaTimeSegment[];
|
||||||
|
recapSegments: MediaTimeSegment[];
|
||||||
|
commercialSegments: MediaTimeSegment[];
|
||||||
|
previewSegments: MediaTimeSegment[];
|
||||||
}> => {
|
}> => {
|
||||||
const introSegments: MediaTimeSegment[] = [];
|
const introSegments: MediaTimeSegment[] = [];
|
||||||
const creditSegments: MediaTimeSegment[] = [];
|
const creditSegments: MediaTimeSegment[] = [];
|
||||||
|
|
||||||
try {
|
const [introRes, creditRes] = await Promise.allSettled([
|
||||||
const [introRes, creditRes] = await Promise.allSettled([
|
api.axiosInstance.get<IntroTimestamps>(
|
||||||
api.axiosInstance.get<IntroTimestamps>(
|
`${api.basePath}/Episode/${itemId}/IntroTimestamps`,
|
||||||
`${api.basePath}/Episode/${itemId}/IntroTimestamps`,
|
{ headers: getAuthHeaders(api) },
|
||||||
{ headers: getAuthHeaders(api) },
|
),
|
||||||
),
|
api.axiosInstance.get<CreditTimestamps>(
|
||||||
api.axiosInstance.get<CreditTimestamps>(
|
`${api.basePath}/Episode/${itemId}/Timestamps`,
|
||||||
`${api.basePath}/Episode/${itemId}/Timestamps`,
|
{ headers: getAuthHeaders(api) },
|
||||||
{ headers: getAuthHeaders(api) },
|
),
|
||||||
),
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
if (introRes.status === "fulfilled" && introRes.value.data.Valid) {
|
if (introRes.status === "fulfilled" && introRes.value.data.Valid) {
|
||||||
introSegments.push({
|
introSegments.push({
|
||||||
startTime: introRes.value.data.IntroStart,
|
startTime: introRes.value.data.IntroStart,
|
||||||
endTime: introRes.value.data.IntroEnd,
|
endTime: introRes.value.data.IntroEnd,
|
||||||
text: "Intro",
|
text: "Intro",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
creditRes.status === "fulfilled" &&
|
|
||||||
creditRes.value.data.Credits.Valid
|
|
||||||
) {
|
|
||||||
creditSegments.push({
|
|
||||||
startTime: creditRes.value.data.Credits.Start,
|
|
||||||
endTime: creditRes.value.data.Credits.End,
|
|
||||||
text: "Credits",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch legacy segments", error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { introSegments, creditSegments };
|
if (creditRes.status === "fulfilled" && creditRes.value.data.Credits.Valid) {
|
||||||
|
creditSegments.push({
|
||||||
|
startTime: creditRes.value.data.Credits.Start,
|
||||||
|
endTime: creditRes.value.data.Credits.End,
|
||||||
|
text: "Credits",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
introSegments,
|
||||||
|
creditSegments,
|
||||||
|
recapSegments: [],
|
||||||
|
commercialSegments: [],
|
||||||
|
previewSegments: [],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchAndParseSegments = async (
|
export const fetchAndParseSegments = async (
|
||||||
@@ -193,6 +227,9 @@ export const fetchAndParseSegments = async (
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
introSegments: MediaTimeSegment[];
|
introSegments: MediaTimeSegment[];
|
||||||
creditSegments: MediaTimeSegment[];
|
creditSegments: MediaTimeSegment[];
|
||||||
|
recapSegments: MediaTimeSegment[];
|
||||||
|
commercialSegments: MediaTimeSegment[];
|
||||||
|
previewSegments: MediaTimeSegment[];
|
||||||
}> => {
|
}> => {
|
||||||
// Try new API first (Jellyfin 10.11+)
|
// Try new API first (Jellyfin 10.11+)
|
||||||
const newSegments = await fetchMediaSegments(itemId, api);
|
const newSegments = await fetchMediaSegments(itemId, api);
|
||||||
|
|||||||
Reference in New Issue
Block a user