Compare commits

..

1 Commits

Author SHA1 Message Date
Fredrik Burmester
e6dd433821 wip 2025-11-14 20:05:39 +01:00
96 changed files with 706 additions and 2197 deletions

View File

@@ -156,7 +156,7 @@ jobs:
build-ios-phone: build-ios-phone:
if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
runs-on: macos-26 runs-on: macos-15
name: 🍎 Build iOS IPA (Phone) name: 🍎 Build iOS IPA (Phone)
permissions: permissions:
contents: read contents: read
@@ -191,11 +191,6 @@ jobs:
- name: 🛠️ Generate project files - name: 🛠️ Generate project files
run: bun run prebuild run: bun run prebuild
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
with:
xcode-version: "26.0.1"
- name: 🏗️ Setup EAS - name: 🏗️ Setup EAS
uses: expo/expo-github-action@main uses: expo/expo-github-action@main
with: with:
@@ -224,7 +219,7 @@ jobs:
# Disabled for now - uncomment when ready to build iOS TV # Disabled for now - uncomment when ready to build iOS TV
# build-ios-tv: # build-ios-tv:
# if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) # if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
# runs-on: macos-26 # runs-on: macos-15
# name: 🍎 Build iOS IPA (TV) # name: 🍎 Build iOS IPA (TV)
# permissions: # permissions:
# contents: read # contents: read
@@ -259,11 +254,6 @@ jobs:
# - name: 🛠️ Generate project files # - name: 🛠️ Generate project files
# run: bun run prebuild:tv # run: bun run prebuild:tv
# #
# - name: 🔧 Setup Xcode
# uses: maxim-lobanov/setup-xcode@v1
# with:
# xcode-version: '26.0.1'
#
# - name: 🏗️ Setup EAS # - name: 🏗️ Setup EAS
# uses: expo/expo-github-action@main # uses: expo/expo-github-action@main
# with: # with:

View File

@@ -31,13 +31,13 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: 🏁 Initialize CodeQL - name: 🏁 Initialize CodeQL
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild - name: 🛠️ Autobuild
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
- name: 🧪 Perform CodeQL Analysis - name: 🧪 Perform CodeQL Analysis
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2

View File

@@ -57,7 +57,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
with: with:
fail-on-severity: high fail-on-severity: high
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }} base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}

177
.vscode/settings.json vendored
View File

@@ -1,25 +1,178 @@
{ {
// ==========================================
// FORMATTING & LINTING
// ==========================================
// Biome as default formatter
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.formatOnPaste": true,
"source.fixAll.biome": "explicit" "editor.formatOnType": false,
// Language-specific formatters
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome",
}, "editor.formatOnSave": true
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
}, },
"[javascriptreact]": { "[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}, },
"typescript.tsdk": "node_modules/typescript/lib", "[jsonc]": {
"typescript.enablePromptUseWorkspaceTsdk": true, "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSaveMode": "file" "editor.formatOnSave": true
},
"[swift]": {
"editor.insertSpaces": true,
"editor.tabSize": 2
},
// ==========================================
// TYPESCRIPT & JAVASCRIPT
// ==========================================
// TypeScript performance optimizations
"typescript.preferences.includePackageJsonAutoImports": "auto",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.preferences.includeCompletionsForImportStatements": true,
"typescript.preferences.includeCompletionsWithSnippetText": true,
// JavaScript settings
"javascript.preferences.importModuleSpecifier": "relative",
"javascript.suggest.autoImports": true,
"javascript.updateImportsOnFileMove.enabled": "always",
// ==========================================
// REACT NATIVE & EXPO
// ==========================================
// File associations for React Native
"files.associations": {
"*.expo.ts": "typescript",
"*.expo.tsx": "typescriptreact",
"*.expo.js": "javascript",
"*.expo.jsx": "javascriptreact",
"metro.config.js": "javascript",
"babel.config.js": "javascript",
"app.config.js": "javascript",
"eas.json": "jsonc"
},
// React Native specific settings
"emmet.includeLanguages": {
"typescriptreact": "html",
"javascriptreact": "html"
},
"emmet.triggerExpansionOnTab": true,
// Exclude build directories from search
"search.exclude": {
"**/node_modules": true
},
// ==========================================
// EDITOR PERFORMANCE & UX
// ==========================================
// Performance optimizations
"editor.largeFileOptimizations": true,
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"**/.expo/**": true,
"**/ios/**": true,
"**/android/**": true,
"**/build/**": true,
"**/dist/**": true
},
// Better editor behavior
"editor.suggestSelection": "first",
"editor.quickSuggestions": {
"strings": true,
"comments": true,
"other": true
},
"editor.snippetSuggestions": "top",
"editor.tabCompletion": "on",
"editor.wordBasedSuggestions": "off",
// ==========================================
// TERMINAL & DEVELOPMENT
// ==========================================
// Terminal settings for Bun (Windows-specific)
"terminal.integrated.profiles.windows": {
"Command Prompt": {
"path": "C:\\Windows\\System32\\cmd.exe",
"env": {
"PATH": "${env:PATH};./node_modules/.bin"
}
}
},
// ==========================================
// WORKSPACE & NAVIGATION
// ==========================================
// Better workspace navigation
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.js",
"*.tsx": "${capture}.js",
"*.js": "${capture}.js,${capture}.js.map,${capture}.min.js,${capture}.d.ts",
"*.jsx": "${capture}.js",
"package.json": "package-lock.json,yarn.lock,bun.lock,bun.lockb,.yarnrc,.yarnrc.yml",
"tsconfig.json": "tsconfig.*.json",
".env": ".env.*",
"app.json": "app.config.js,eas.json,expo-env.d.ts",
"README.md": "LICENSE.txt,SECURITY.md,CODE_OF_CONDUCT.md,CONTRIBUTING.md"
},
// Better breadcrumbs and navigation
"breadcrumbs.enabled": true,
"outline.showVariables": true,
"outline.showConstants": true,
// ==========================================
// GIT & VERSION CONTROL
// ==========================================
// Git integration
"git.autofetch": true,
"git.enableSmartCommit": true,
"git.confirmSync": false,
"git.ignoreLimitWarning": true,
// ==========================================
// CODE QUALITY & ERRORS
// ==========================================
// Better error detection
"typescript.validate.enable": true,
"javascript.validate.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
// Problem matcher for better error display
"typescript.tsc.autoDetect": "on"
} }

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Streamyfin", "name": "Streamyfin",
"slug": "streamyfin", "slug": "streamyfin",
"version": "0.48.0", "version": "0.47.1",
"orientation": "default", "orientation": "default",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "streamyfin", "scheme": "streamyfin",
@@ -29,12 +29,16 @@
}, },
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.fredrikburmester.streamyfin", "bundleIdentifier": "com.fredrikburmester.streamyfin",
"icon": "./assets/images/icon-ios-liquid-glass.icon", "icon": {
"dark": "./assets/images/icon-ios-plain.png",
"light": "./assets/images/icon-ios-light.png",
"tinted": "./assets/images/icon-ios-tinted.png"
},
"appleTeamId": "MWD5K362T8" "appleTeamId": "MWD5K362T8"
}, },
"android": { "android": {
"jsEngine": "hermes", "jsEngine": "hermes",
"versionCode": 85, "versionCode": 84,
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png", "foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png", "monochromeImage": "./assets/images/icon-android-themed.png",

View File

@@ -171,7 +171,7 @@ export default function page() {
contentInsetAdjustmentBehavior='automatic' contentInsetAdjustmentBehavior='automatic'
> >
<View style={{ paddingTop: Platform.OS === "android" ? 17 : 0 }}> <View style={{ paddingTop: Platform.OS === "android" ? 17 : 0 }}>
<View className='mb-4 flex flex-col space-y-4 px-4'> <View className='mb-4 flex flex-col gap-y-4 px-4'>
{/* Queue card - hidden */} {/* Queue card - hidden */}
{/* <View className='bg-neutral-900 p-4 rounded-2xl'> {/* <View className='bg-neutral-900 p-4 rounded-2xl'>
<Text className='text-lg font-bold'> <Text className='text-lg font-bold'>
@@ -180,7 +180,7 @@ export default function page() {
<Text className='text-xs opacity-70 text-red-600'> <Text className='text-xs opacity-70 text-red-600'>
{t("home.downloads.queue_hint")} {t("home.downloads.queue_hint")}
</Text> </Text>
<View className='flex flex-col space-y-2 mt-2'> <View className='flex flex-col gap-y-2 mt-2'>
{queue.map((q, index) => ( {queue.map((q, index) => (
<TouchableOpacity <TouchableOpacity
onPress={() => onPress={() =>

View File

@@ -20,7 +20,7 @@ export default function page() {
return ( return (
<View <View
className={`bg-neutral-900 h-full ${Platform.isTV ? "py-5 space-y-4" : "py-16 space-y-8"} px-4`} className={`bg-neutral-900 h-full ${Platform.isTV ? "py-5 gap-y-4" : "py-16 gap-y-8"} px-4`}
> >
<View> <View>
<Text className='text-3xl font-bold text-center mb-2'> <Text className='text-3xl font-bold text-center mb-2'>

View File

@@ -255,7 +255,7 @@ const SessionCard = ({ session }: SessionCardProps) => {
</View> </View>
{/* Session controls */} {/* Session controls */}
<View className='flex flex-row mt-2 space-x-4 justify-center'> <View className='flex flex-row mt-2 gap-x-4 justify-center'>
<TouchableOpacity <TouchableOpacity
onPress={handlePrevious} onPress={handlePrevious}
disabled={isControlLoading[PlaystateCommand.PreviousTrack]} disabled={isControlLoading[PlaystateCommand.PreviousTrack]}

View File

@@ -1,9 +1,9 @@
import { File, Paths } from "expo-file-system"; import { File, Paths } from "expo-file-system";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import type * as SharingType from "expo-sharing"; import * as Sharing from "expo-sharing";
import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { useCallback, useEffect, useId, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; import { ScrollView, TouchableOpacity, View } from "react-native";
import Collapsible from "react-native-collapsible"; import Collapsible from "react-native-collapsible";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
@@ -11,11 +11,6 @@ import { FilterButton } from "@/components/filters/FilterButton";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { LogLevel, useLog, writeErrorLog } from "@/utils/log"; import { LogLevel, useLog, writeErrorLog } from "@/utils/log";
// Conditionally import expo-sharing only on non-TV platforms
const Sharing = Platform.isTV
? null
: (require("expo-sharing") as typeof SharingType);
export default function Page() { export default function Page() {
const navigation = useNavigation(); const navigation = useNavigation();
const { logs } = useLog(); const { logs } = useLog();
@@ -54,8 +49,6 @@ export default function Page() {
// Sharing it as txt while its formatted allows us to share it with many more applications // Sharing it as txt while its formatted allows us to share it with many more applications
const share = useCallback(async () => { const share = useCallback(async () => {
if (!Sharing) return;
const logsFile = new File(Paths.document, "logs.txt"); const logsFile = new File(Paths.document, "logs.txt");
setLoading(true); setLoading(true);
@@ -67,11 +60,9 @@ export default function Page() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [filteredLogs, Sharing]); }, [filteredLogs]);
useEffect(() => { useEffect(() => {
if (Platform.isTV) return;
navigation.setOptions({ navigation.setOptions({
headerRight: () => headerRight: () =>
loading ? ( loading ? (
@@ -91,7 +82,7 @@ export default function Page() {
paddingTop: insets.top + 48, paddingTop: insets.top + 48,
}} }}
> >
<View className='flex flex-row justify-end py-2 px-4 space-x-2'> <View className='flex flex-row justify-end py-2 px-4 gap-x-2'>
<FilterButton <FilterButton
id={orderFilterId} id={orderFilterId}
queryKey='log' queryKey='log'
@@ -115,7 +106,7 @@ export default function Page() {
/> />
</View> </View>
<ScrollView className='pb-4 px-4'> <ScrollView className='pb-4 px-4'>
<View className='flex flex-col space-y-2'> <View className='flex flex-col gap-y-2'>
{filteredLogs?.map((log, index) => ( {filteredLogs?.map((log, index) => (
<View className='bg-neutral-900 rounded-xl p-3' key={index}> <View className='bg-neutral-900 rounded-xl p-3' key={index}>
<TouchableOpacity <TouchableOpacity
@@ -155,7 +146,7 @@ export default function Page() {
</Text> </Text>
)} )}
<Collapsible collapsed={!state[log.timestamp]}> <Collapsible collapsed={!state[log.timestamp]}>
<View className='mt-2 flex flex-col space-y-2'> <View className='mt-2 flex flex-col gap-y-2'>
<ScrollView className='rounded-xl' style={codeBlockStyle}> <ScrollView className='rounded-xl' style={codeBlockStyle}>
<Text>{JSON.stringify(log.data, null, 2)}</Text> <Text>{JSON.stringify(log.data, null, 2)}</Text>
</ScrollView> </ScrollView>

View File

@@ -1,4 +1,3 @@
import { ItemFields } from "@jellyfin/sdk/lib/generated-client/models";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import type React from "react"; import type React from "react";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -21,11 +20,7 @@ const Page: React.FC = () => {
const { offline } = useLocalSearchParams() as { offline?: string }; const { offline } = useLocalSearchParams() as { offline?: string };
const isOffline = offline === "true"; const isOffline = offline === "true";
const { data: item, isError } = useItemQuery(id, false, undefined, [ const { data: item, isError } = useItemQuery(id, isOffline);
ItemFields.MediaSources,
ItemFields.MediaSourceCount,
ItemFields.MediaStreams,
]);
const opacity = useSharedValue(1); const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => {
@@ -85,7 +80,7 @@ const Page: React.FC = () => {
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' /> <View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' /> <View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' /> <View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
<View className='flex flex-row space-x-1 mb-8'> <View className='flex flex-row gap-x-1 mb-8'>
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' /> <View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' /> <View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' /> <View className='h-6 bg-neutral-900 rounded mb-3 w-14' />

View File

@@ -131,11 +131,9 @@ const Page: React.FC = () => {
mediaId: Number(result.id!), mediaId: Number(result.id!),
mediaType: mediaType!, mediaType: mediaType!,
tvdbId: details?.externalIds?.tvdbId, tvdbId: details?.externalIds?.tvdbId,
...(mediaType === MediaType.TV && { seasons: (details as TvDetails)?.seasons
seasons: (details as TvDetails)?.seasons ?.filter?.((s) => s.seasonNumber !== 0)
?.filter?.((s) => s.seasonNumber !== 0) ?.map?.((s) => s.seasonNumber),
?.map?.((s) => s.seasonNumber),
}),
}; };
if (hasAdvancedRequestPermission) { if (hasAdvancedRequestPermission) {
@@ -241,7 +239,7 @@ const Page: React.FC = () => {
} }
> >
<View className='flex flex-col'> <View className='flex flex-col'>
<View className='space-y-4'> <View className='gap-y-4'>
<View className='px-4'> <View className='px-4'>
<View className='flex flex-row justify-between w-full'> <View className='flex flex-row justify-between w-full'>
<View className='flex flex-col w-56'> <View className='flex flex-col w-56'>
@@ -284,7 +282,7 @@ const Page: React.FC = () => {
</Button> </Button>
) : ( ) : (
details?.mediaInfo?.jellyfinMediaId && ( details?.mediaInfo?.jellyfinMediaId && (
<View className='flex flex-row space-x-2 mt-4'> <View className='flex flex-row gap-x-2 mt-4'>
{!Platform.isTV && ( {!Platform.isTV && (
<Button <Button
className='flex-1 bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100' className='flex-1 bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100'
@@ -384,13 +382,13 @@ const Page: React.FC = () => {
onDismiss={handleIssueModalDismiss} onDismiss={handleIssueModalDismiss}
> >
<BottomSheetView> <BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col gap-y-4 px-4 pb-8 pt-2'>
<View> <View>
<Text className='font-bold text-2xl text-neutral-100'> <Text className='font-bold text-2xl text-neutral-100'>
{t("jellyseerr.whats_wrong")} {t("jellyseerr.whats_wrong")}
</Text> </Text>
</View> </View>
<View className='flex flex-col space-y-2 items-start'> <View className='flex flex-col gap-y-2 items-start'>
<View className='flex flex-col w-full'> <View className='flex flex-col w-full'>
<Text className='opacity-50 mb-1 text-xs'> <Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.issue_type")} {t("jellyseerr.issue_type")}

View File

@@ -26,7 +26,7 @@ export default function page() {
paddingTop: 8, paddingTop: 8,
}} }}
> >
<View className='flex flex-col space-y-2'> <View className='flex flex-col gap-y-2'>
<ScrollingCollectionList <ScrollingCollectionList
queryKey={["livetv", "recommended"]} queryKey={["livetv", "recommended"]}
title={t("live_tv.on_now")} title={t("live_tv.on_now")}

View File

@@ -105,7 +105,7 @@ const page: React.FC = () => {
/> />
} }
> >
<View className='flex flex-col space-y-4 my-4'> <View className='flex flex-col gap-y-4 my-4'>
<View className='px-4 mb-4'> <View className='px-4 mb-4'>
<MoviesTitleHeader item={item} className='mb-4' /> <MoviesTitleHeader item={item} className='mb-4' />
<OverviewText text={item.Overview} /> <OverviewText text={item.Overview} />

View File

@@ -94,7 +94,7 @@ const page: React.FC = () => {
item && item &&
allEpisodes && allEpisodes &&
allEpisodes.length > 0 && ( allEpisodes.length > 0 && (
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
<AddToFavorites item={item} /> <AddToFavorites item={item} />
{!Platform.isTV && ( {!Platform.isTV && (
<DownloadItems <DownloadItems

View File

@@ -418,7 +418,7 @@ export default function search() {
</Text> </Text>
</View> </View>
) : debouncedSearch.length === 0 ? ( ) : debouncedSearch.length === 0 ? (
<View className='mt-4 flex flex-col items-center space-y-2'> <View className='mt-4 flex flex-col items-center gap-y-2'>
{exampleSearches.map((e) => ( {exampleSearches.map((e) => (
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {

View File

@@ -43,7 +43,6 @@ import { useDownload } from "@/providers/DownloadProvider";
import { DownloadedItem } from "@/providers/Downloads/types"; import { DownloadedItem } from "@/providers/Downloads/types";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import { generateDeviceProfile } from "@/utils/profiles/native"; import { generateDeviceProfile } from "@/utils/profiles/native";
@@ -674,30 +673,7 @@ export default function page() {
); );
}, []); }, []);
// Prepare metadata for iOS native media controls console.log("Debug: component render"); // Uncomment to debug re-renders
const nowPlayingMetadata = useMemo(() => {
if (!item || !api) return undefined;
const artworkUri = getPrimaryImageUrl({
api,
item,
quality: 90,
width: 500,
});
return {
title: item.Name || "",
artist:
item.Type === "Episode"
? item.SeriesName || ""
: item.AlbumArtist || "",
albumTitle:
item.Type === "Episode" && item.SeasonName
? item.SeasonName
: undefined,
artworkUri: artworkUri || undefined,
};
}, [item, api]);
// Show error UI first, before checking loading/missingdata // Show error UI first, before checking loading/missingdata
if (itemStatus.isError || streamStatus.isError) { if (itemStatus.isError || streamStatus.isError) {
@@ -755,7 +731,6 @@ export default function page() {
initOptions, initOptions,
}} }}
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
nowPlayingMetadata={nowPlayingMetadata}
onVideoProgress={onProgress} onVideoProgress={onProgress}
progressUpdateInterval={1000} progressUpdateInterval={1000}
onVideoStateChange={onPlaybackStateChanged} onVideoStateChange={onPlaybackStateChanged}

View File

@@ -1,19 +1,17 @@
import { Link, Stack } from "expo-router"; import { Link, Stack } from "expo-router";
import { StyleSheet } from "react-native"; import { StyleSheet, View } from "react-native";
import { Text } from "../components/common/Text";
import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
export default function NotFoundScreen() { export default function NotFoundScreen() {
return ( return (
<> <>
<Stack.Screen options={{ title: "Oops!" }} /> <Stack.Screen options={{ title: "Oops!" }} />
<ThemedView style={styles.container}> <View style={styles.container}>
<ThemedText type='title'>This screen doesn't exist.</ThemedText> <Text>This screen doesn't exist.</Text>
<Link href={"/home"} style={styles.link}> <Link href={"/home"} style={styles.link}>
<ThemedText type='link'>Go to home screen!</ThemedText> <Text>Go to home screen!</Text>
</Link> </Link>
</ThemedView> </View>
</> </>
); );
} }

View File

@@ -34,6 +34,7 @@ import { storage } from "@/utils/mmkv";
const Notifications = !Platform.isTV ? require("expo-notifications") : null; const Notifications = !Platform.isTV ? require("expo-notifications") : null;
import "@/global.css";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { getLocales } from "expo-localization"; import { getLocales } from "expo-localization";
import type { EventSubscription } from "expo-modules-core"; import type { EventSubscription } from "expo-modules-core";

View File

@@ -42,14 +42,14 @@ const Login: React.FC = () => {
const [loadingServerCheck, setLoadingServerCheck] = useState<boolean>(false); const [loadingServerCheck, setLoadingServerCheck] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [serverURL, setServerURL] = useState<string>(_apiUrl || ""); const [serverURL, setServerURL] = useState<string>(_apiUrl);
const [serverName, setServerName] = useState<string>(""); const [serverName, setServerName] = useState<string>("");
const [credentials, setCredentials] = useState<{ const [credentials, setCredentials] = useState<{
username: string; username: string;
password: string; password: string;
}>({ }>({
username: _username || "", username: _username,
password: _password || "", password: _password,
}); });
/** /**
@@ -264,12 +264,6 @@ const Login: React.FC = () => {
onChangeText={(text: string) => onChangeText={(text: string) =>
setCredentials({ ...credentials, username: text }) setCredentials({ ...credentials, username: text })
} }
onEndEditing={(e) => {
const newValue = e.nativeEvent.text;
if (newValue && newValue !== credentials.username) {
setCredentials({ ...credentials, username: newValue });
}
}}
value={credentials.username} value={credentials.username}
keyboardType='default' keyboardType='default'
returnKeyType='done' returnKeyType='done'
@@ -278,8 +272,6 @@ const Login: React.FC = () => {
clearButtonMode='while-editing' clearButtonMode='while-editing'
maxLength={500} maxLength={500}
extraClassName='mb-4' extraClassName='mb-4'
autoFocus={false}
blurOnSubmit={true}
/> />
{/* Password */} {/* Password */}
@@ -288,12 +280,6 @@ const Login: React.FC = () => {
onChangeText={(text: string) => onChangeText={(text: string) =>
setCredentials({ ...credentials, password: text }) setCredentials({ ...credentials, password: text })
} }
onEndEditing={(e) => {
const newValue = e.nativeEvent.text;
if (newValue && newValue !== credentials.password) {
setCredentials({ ...credentials, password: newValue });
}
}}
value={credentials.password} value={credentials.password}
secureTextEntry secureTextEntry
keyboardType='default' keyboardType='default'
@@ -303,17 +289,10 @@ const Login: React.FC = () => {
clearButtonMode='while-editing' clearButtonMode='while-editing'
maxLength={500} maxLength={500}
extraClassName='mb-4' extraClassName='mb-4'
autoFocus={false}
blurOnSubmit={true}
/> />
<View className='mt-4'> <View className='mt-4'>
<Button <Button onPress={handleLogin}>{t("login.login_button")}</Button>
onPress={handleLogin}
disabled={!credentials.username.trim()}
>
{t("login.login_button")}
</Button>
</View> </View>
<View className='mt-3'> <View className='mt-3'>
<Button <Button
@@ -355,8 +334,6 @@ const Login: React.FC = () => {
autoCapitalize='none' autoCapitalize='none'
textContentType='URL' textContentType='URL'
maxLength={500} maxLength={500}
autoFocus={false}
blurOnSubmit={true}
/> />
{/* Full-width primary button */} {/* Full-width primary button */}
@@ -400,7 +377,7 @@ const Login: React.FC = () => {
{api?.basePath ? ( {api?.basePath ? (
<View className='flex flex-col flex-1 items-center justify-center'> <View className='flex flex-col flex-1 items-center justify-center'>
<View className='px-4 -mt-20 w-full'> <View className='px-4 -mt-20 w-full'>
<View className='flex flex-col space-y-2'> <View className='flex flex-col gap-y-2'>
<Text className='text-2xl font-bold -mb-2'> <Text className='text-2xl font-bold -mb-2'>
{serverName ? ( {serverName ? (
<> <>
@@ -417,12 +394,6 @@ const Login: React.FC = () => {
onChangeText={(text) => onChangeText={(text) =>
setCredentials({ ...credentials, username: text }) setCredentials({ ...credentials, username: text })
} }
onEndEditing={(e) => {
const newValue = e.nativeEvent.text;
if (newValue && newValue !== credentials.username) {
setCredentials({ ...credentials, username: newValue });
}
}}
value={credentials.username} value={credentials.username}
keyboardType='default' keyboardType='default'
returnKeyType='done' returnKeyType='done'
@@ -439,12 +410,6 @@ const Login: React.FC = () => {
onChangeText={(text) => onChangeText={(text) =>
setCredentials({ ...credentials, password: text }) setCredentials({ ...credentials, password: text })
} }
onEndEditing={(e) => {
const newValue = e.nativeEvent.text;
if (newValue && newValue !== credentials.password) {
setCredentials({ ...credentials, password: newValue });
}
}}
value={credentials.password} value={credentials.password}
secureTextEntry secureTextEntry
keyboardType='default' keyboardType='default'
@@ -458,7 +423,6 @@ const Login: React.FC = () => {
<Button <Button
onPress={handleLogin} onPress={handleLogin}
loading={loading} loading={loading}
disabled={!credentials.username.trim()}
className='flex-1 mr-2' className='flex-1 mr-2'
> >
{t("login.login_button")} {t("login.login_button")}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 384 415" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.133333,0,0,-0.133333,-110.933,512.698)">
<g id="g10">
<path id="path88" d="M3547.01,1831.49C3493.38,1822.66 3262.53,1779.28 2992.01,1820.24C2424.16,1906.21 2154.85,2275.8 1882,2420.24C1473.31,2636.6 1060.97,2644.95 832,2592.03L832,1445.92C832,1321.76 863.078,1198.06 925.307,1090.27C1057.09,862.011 1323.38,718.405 1586.6,736.145C1695.48,743.482 1801.3,777.735 1895.64,832.199L3357.51,1676.21C3424.47,1714.87 3482.92,1761.76 3532.01,1815.41L3547.01,1831.49Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2879.19,0,0,2879.19,832.651,2289.93)"><stop offset="0" style="stop-color:rgb(149,41,235);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(98,22,247);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 384 415" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.133333,0,0,-0.133333,-110.933,512.698)">
<g id="g10">
<path id="path66" d="M3357.51,2903.64L1895.64,3747.65C1670.29,3877.76 1412.33,3877.76 1186.98,3747.65C961.629,3617.55 832.648,3394.14 832.648,3133.93L832.648,1445.92C832.648,1185.71 961.629,962.305 1186.98,832.199C1412.33,702.094 1670.29,702.094 1895.64,832.199L3357.51,1676.21C3582.86,1806.31 3711.84,2029.71 3711.84,2289.93C3711.84,2550.14 3582.86,2773.54 3357.51,2903.64ZM1721.48,3213.68L3098.31,2454.7C3163.9,2418.55 3193.45,2364.85 3193.45,2289.93C3193.45,2215 3163.93,2161.32 3098.31,2125.15L1721.48,1366.18C1655.87,1330.01 1596.09,1328.72 1531.21,1366.18C1466.34,1403.63 1436.08,1456.03 1436.08,1530.96L1436.08,3048.89C1436.08,3123.77 1466.35,3176.23 1531.21,3213.68C1596.08,3251.11 1655.89,3249.83 1721.48,3213.68" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2879.19,0,0,2879.19,832.651,2289.93)"><stop offset="0" style="stop-color:rgb(188,74,241);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(227,105,219);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 384 415" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<g id="g10">
<path id="path88" d="M0,319.909L0,234C17.667,234.844 138.649,236.708 195,190C220.441,168.912 271.21,169.515 294.001,178.788C332.576,194.487 378.643,259.549 360,270.644C353.455,277.797 345.662,284.049 336.734,289.204L141.818,401.738C129.24,409 115.13,413.567 100.613,414.546C65.517,416.911 30.012,397.763 12.441,367.329C4.144,352.957 0,336.464 0,319.909Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2879.19,0,0,2879.19,832.651,2289.93)"><stop offset="0" style="stop-color:rgb(225,102,222);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(204,88,233);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 384 415" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.133333,0,0,-0.133333,-110.933,512.698)">
<g id="g10">
<path id="path28" d="M1427.29,1523.37C1427.29,1447.7 1457.85,1394.77 1523.38,1356.94C1588.91,1319.11 1649.28,1320.41 1715.55,1356.94L3106.14,2123.5C3172.42,2160.03 3202.24,2214.25 3202.24,2289.93C3202.24,2365.6 3172.39,2419.83 3106.14,2456.35L1715.55,3222.91C1649.31,3259.43 1588.89,3260.73 1523.38,3222.91C1457.87,3185.1 1427.29,3132.11 1427.29,3056.48L1427.29,1523.37" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.17673e-13,-1921.74,1921.74,1.17673e-13,2314.76,3250.79)"><stop offset="0" style="stop-color:rgb(93,17,250);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(143,40,236);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,184 +0,0 @@
{
"fill": {
"solid": "display-p3:0.18039,0.18039,0.18039,1.00000"
},
"groups": [
{
"blur-material": 0.3,
"layers": [
{
"fill-specializations": [
{
"value": "none"
},
{
"appearance": "tinted",
"value": {
"automatic-gradient": "display-p3:0.76482,0.76482,0.76482,0.84903"
}
}
],
"glass": true,
"hidden": false,
"image-name": "streamyfin_logo_layer1.svg",
"name": "streamyfin_logo_layer1"
}
],
"opacity": 1,
"position": {
"scale": 1.7,
"translation-in-points": [30, 0]
},
"shadow": {
"kind": "none",
"opacity": 1
},
"specular": true,
"translucency": {
"enabled": true,
"value": 0.6
}
},
{
"blend-mode": "normal",
"blur-material": 0.8,
"hidden": false,
"layers": [
{
"blend-mode": "normal",
"fill-specializations": [
{
"value": "none"
},
{
"appearance": "tinted",
"value": {
"automatic-gradient": "gray:0.75000,1.00000"
}
}
],
"hidden": false,
"image-name": "streamyfin_logo_layer2.svg",
"name": "streamyfin_logo_layer2",
"opacity": 1,
"position": {
"scale": 1,
"translation-in-points": [0, 0]
}
}
],
"lighting": "individual",
"name": "Group",
"opacity": 1,
"position": {
"scale": 1.7,
"translation-in-points": [30, -0.01613253252572302]
},
"shadow": {
"kind": "layer-color",
"opacity": 0.35
},
"specular": true,
"translucency-specializations": [
{
"value": {
"enabled": true,
"value": 0.5
}
},
{
"appearance": "tinted",
"value": {
"enabled": true,
"value": 0.8
}
}
]
},
{
"blend-mode": "normal",
"blur-material": 0.5,
"layers": [
{
"fill-specializations": [
{
"appearance": "tinted",
"value": {
"automatic-gradient": "gray:0.29000,1.00000"
}
}
],
"glass": true,
"hidden": false,
"image-name": "streamyfin_logo_layer3.svg",
"name": "streamyfin_logo_layer3",
"opacity": 0.9
}
],
"name": "Group",
"opacity": 0.8,
"position": {
"scale": 1.7,
"translation-in-points": [30, 0]
},
"shadow": {
"kind": "none",
"opacity": 0.5
},
"specular": true,
"translucency": {
"enabled": true,
"value": 0.7
}
},
{
"blur-material": 0.5,
"hidden": false,
"layers": [
{
"glass": true,
"hidden-specializations": [
{
"value": false
},
{
"appearance": "tinted",
"value": true
}
],
"image-name": "streamyfin_logo_layer4.svg",
"name": "streamyfin_logo_layer4",
"opacity-specializations": [
{
"value": 1
},
{
"appearance": "tinted",
"value": 0
}
]
}
],
"lighting": "combined",
"name": "Group",
"opacity": 0.9,
"position": {
"scale": 1.7,
"translation-in-points": [30, 0]
},
"shadow": {
"kind": "neutral",
"opacity": 0.5
},
"specular": false,
"translucency": {
"enabled": true,
"value": 0.5
}
}
],
"supported-platforms": {
"circles": ["watchOS"],
"squares": "shared"
}
}

View File

@@ -2,6 +2,6 @@ module.exports = (api) => {
api.cache(true); api.cache(true);
return { return {
presets: ["babel-preset-expo"], presets: ["babel-preset-expo"],
plugins: ["nativewind/babel", "react-native-worklets/plugin"], plugins: ["react-native-worklets/plugin"],
}; };
}; };

217
bun.lock
View File

@@ -46,18 +46,19 @@
"i18next": "^25.0.0", "i18next": "^25.0.0",
"jotai": "^2.12.5", "jotai": "^2.12.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nativewind": "^2.0.11", "nativewind": "^5.0.0-preview.2",
"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.0.0", "react-i18next": "^15.4.0",
"react-native": "npm:react-native-tvos@0.81.5-1", "react-native": "npm:react-native-tvos@0.81.5-1",
"react-native-awesome-slider": "^2.9.0", "react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "^1.0.2", "react-native-bottom-tabs": "^1.0.2",
"react-native-circular-progress": "^1.4.1", "react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2", "react-native-collapsible": "^1.6.2",
"react-native-country-flag": "^2.0.2", "react-native-country-flag": "^2.0.2",
"react-native-device-info": "^15.0.0", "react-native-css": "^3.0.1",
"react-native-device-info": "^14.0.4",
"react-native-edge-to-edge": "^1.7.0", "react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-google-cast": "^4.9.1", "react-native-google-cast": "^4.9.1",
@@ -80,31 +81,33 @@
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1", "react-native-worklets": "0.5.1",
"sonner-native": "^0.21.0", "sonner-native": "^0.21.0",
"tailwindcss": "3.3.2", "tailwindcss": "^4.1.17",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"zod": "^4.1.3", "zod": "^4.1.3",
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.5", "@babel/core": "^7.20.0",
"@biomejs/biome": "2.3.5", "@biomejs/biome": "^2.3.5",
"@react-native-community/cli": "20.0.2", "@react-native-community/cli": "^20.0.0",
"@react-native-tvos/config-tv": "0.1.4", "@react-native-tvos/config-tv": "^0.1.1",
"@types/jest": "29.5.14", "@tailwindcss/postcss": "^4.1.17",
"@types/lodash": "4.17.20", "@types/jest": "^29.5.12",
"@types/lodash": "^4.17.15",
"@types/react": "~19.1.10", "@types/react": "~19.1.10",
"@types/react-test-renderer": "19.1.0", "@types/react-test-renderer": "^19.0.0",
"cross-env": "10.1.0", "cross-env": "^10.0.0",
"expo-doctor": "1.17.11", "expo-doctor": "^1.17.0",
"husky": "9.1.7", "husky": "^9.1.7",
"lint-staged": "16.2.6", "lint-staged": "^16.1.5",
"postcss": "^8.5.6",
"postinstall-postinstall": "^2.1.0",
"react-test-renderer": "19.1.1", "react-test-renderer": "19.1.1",
"typescript": "5.9.3", "typescript": "~5.9.2",
}, },
}, },
}, },
"overrides": { "overrides": {
"expo-constants": "~18.0.10", "lightningcss": "1.30.1",
"expo-task-manager": "~14.0.8",
}, },
"packages": { "packages": {
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="],
@@ -133,7 +136,7 @@
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
@@ -577,6 +580,36 @@
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="], "@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="], "@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="],
@@ -687,6 +720,8 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="],
"asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="], "assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
@@ -713,7 +748,7 @@
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.1.0-rc.1-rc-af1b7da-20250421", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-E3kaokBhWDLf7ZD8fuYjYn0ZJHYZ+3EHtAWCdX2hl4lpu1z9S/Xr99sxhx2bTCVB41oIesz9FtM8f4INsrZaOw=="],
"babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="], "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="],
@@ -739,8 +774,6 @@
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="], "bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
@@ -777,16 +810,10 @@
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="], "caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="],
@@ -817,12 +844,16 @@
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"colorjs.io": ["colorjs.io@0.6.0-alpha.1", "", {}, "sha512-c/h/8uAmPydQcriRdX8UTAFHj6SpSHFHBA8LvMikvYWAVApPTwg/pyOXNsGmaCBd6L/EeDlRHSNhTtnIFp/qsg=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="],
"commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"comment-json": ["comment-json@4.4.1", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1" } }, "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg=="],
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
@@ -837,6 +868,8 @@
"core-js-compat": ["core-js-compat@3.46.0", "", { "dependencies": { "browserslist": "^4.26.3" } }, "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law=="], "core-js-compat": ["core-js-compat@3.46.0", "", { "dependencies": { "browserslist": "^4.26.3" } }, "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
"cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
@@ -847,22 +880,14 @@
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
"css-in-js-utils": ["css-in-js-utils@3.1.0", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="], "css-in-js-utils": ["css-in-js-utils@3.1.0", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="],
"css-mediaquery": ["css-mediaquery@0.1.2", "", {}, "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
"css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], "css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
@@ -895,12 +920,8 @@
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -925,6 +946,8 @@
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="], "env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="],
@@ -1051,8 +1074,6 @@
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1131,7 +1152,7 @@
"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=="], "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=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="], "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="],
@@ -1199,8 +1220,6 @@
"is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
@@ -1267,7 +1286,7 @@
"jimp-compact": ["jimp-compact@0.16.1", "", {}, "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww=="], "jimp-compact": ["jimp-compact@0.16.1", "", {}, "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww=="],
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
@@ -1307,31 +1326,27 @@
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@@ -1357,6 +1372,8 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="],
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
@@ -1431,7 +1448,7 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nativewind": ["nativewind@2.0.11", "", { "dependencies": { "@babel/generator": "^7.18.7", "@babel/helper-module-imports": "7.18.6", "@babel/types": "7.19.0", "css-mediaquery": "^0.1.2", "css-to-react-native": "^3.0.0", "micromatch": "^4.0.5", "postcss": "^8.4.12", "postcss-calc": "^8.2.4", "postcss-color-functional-notation": "^4.2.2", "postcss-css-variables": "^0.18.0", "postcss-nested": "^5.0.6", "react-is": "^18.1.0", "use-sync-external-store": "^1.1.0" }, "peerDependencies": { "tailwindcss": "~3" } }, "sha512-qCEXUwKW21RYJ33KRAJl3zXq2bCq82WoI564fI21D/TiqhfmstZOqPN53RF8qK1NDK6PGl56b2xaTxgObEePEg=="], "nativewind": ["nativewind@5.0.0-preview.2", "", { "dependencies": { "tailwindcss-safe-area": "^1.1.0" }, "peerDependencies": { "react-native-css": "^3.0.1", "tailwindcss": ">4.1.11" } }, "sha512-rTNrwFIwl/n2VH7KPvsZj/NdvKf+uGHF4NYtPamr5qG2eTYGT8B8VeyCPfYf/xUskpWOLJVqVEXaFO/vuIDEdw=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
@@ -1465,8 +1482,6 @@
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="], "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
@@ -1527,8 +1542,6 @@
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pixelmatch": ["pixelmatch@4.0.2", "", { "dependencies": { "pngjs": "^3.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA=="], "pixelmatch": ["pixelmatch@4.0.2", "", { "dependencies": { "pngjs": "^3.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA=="],
@@ -1541,24 +1554,10 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="],
"postcss-color-functional-notation": ["postcss-color-functional-notation@4.2.4", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg=="],
"postcss-css-variables": ["postcss-css-variables@0.18.0", "", { "dependencies": { "balanced-match": "^1.0.0", "escape-string-regexp": "^1.0.3", "extend": "^3.0.1" }, "peerDependencies": { "postcss": "^8.2.6" } }, "sha512-lYS802gHbzn1GI+lXvy9MYIYDuGnl1WB4FTKoqMQqJ3Mab09A7a/1wZvGTkCEZJTM8mSbIyb1mJYn8f0aPye0Q=="],
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
"postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
"postcss-nested": ["postcss-nested@5.0.6", "", { "dependencies": { "postcss-selector-parser": "^6.0.6" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA=="],
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"postinstall-postinstall": ["postinstall-postinstall@2.1.0", "", {}, "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ=="],
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
@@ -1605,7 +1604,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.0.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q=="], "react-i18next": ["react-i18next@15.7.4", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw=="],
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="], "react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
@@ -1621,7 +1620,9 @@
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="], "react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
"react-native-device-info": ["react-native-device-info@15.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-U5waZRXtT3l1SgZpZMlIvMKPTkFZPH8W7Ks6GrJhdH723aUIPxjVer7cRSij1mvQdOAAYFJV/9BDzlC8apG89A=="], "react-native-css": ["react-native-css@3.0.1", "", { "dependencies": { "babel-plugin-react-compiler": "^19.1.0-rc.2", "colorjs.io": "0.6.0-alpha.1", "comment-json": "^4.2.5", "debug": "^4.4.1" }, "peerDependencies": { "@expo/metro-config": ">=54", "lightningcss": ">=1.27.0", "react": ">=19", "react-native": ">=0.81" } }, "sha512-sE/Qp2p+UaV9+W6T+GwsRH5sIynaTfhM2Khn+tN1q1YKYq1STVyAWyC+0i6ac4mFAO/FxQ/a03BDmrrt48qiuQ=="],
"react-native-device-info": ["react-native-device-info@14.1.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-lXFpe6DJmzbQXNLWxlMHP2xuTU5gwrKAvI8dCAZuERhW9eOXSubOQIesk9lIBnsi9pI19GMrcpJEvs4ARPRYmw=="],
"react-native-edge-to-edge": ["react-native-edge-to-edge@1.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ=="], "react-native-edge-to-edge": ["react-native-edge-to-edge@1.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ=="],
@@ -1679,14 +1680,10 @@
"react-test-renderer": ["react-test-renderer@19.1.1", "", { "dependencies": { "react-is": "^19.1.1", "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-aGRXI+zcBTtg0diHofc7+Vy97nomBs9WHHFY1Csl3iV0x6xucjNYZZAkiVKGiNYUv23ecOex5jE67t8ZzqYObA=="], "react-test-renderer": ["react-test-renderer@19.1.1", "", { "dependencies": { "react-is": "^19.1.1", "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-aGRXI+zcBTtg0diHofc7+Vy97nomBs9WHHFY1Csl3iV0x6xucjNYZZAkiVKGiNYUv23ecOex5jE67t8ZzqYObA=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
@@ -1845,7 +1842,11 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
"tailwindcss-safe-area": ["tailwindcss-safe-area@1.1.0", "", { "peerDependencies": { "tailwindcss": "^4.0.0" } }, "sha512-wuPUeW5BhWNv9yr3OzaGgpqImQG9FBM4mQIQh2C6yjHmOOZsJ3gh5RfNHt+TM16TMtpQs/8k2TWx8yQTFG7Fcw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
@@ -1871,8 +1872,6 @@
"tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
"to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@@ -2001,16 +2000,8 @@
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
"@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/plugin-transform-runtime/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@expo/cli/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], "@expo/cli/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
"@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -2119,6 +2110,18 @@
"@react-navigation/native-stack/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "@react-navigation/native-stack/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], "ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
@@ -2131,14 +2134,12 @@
"babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"babel-preset-expo/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], "babel-preset-expo/babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"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=="], "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2157,8 +2158,6 @@
"expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], "fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="],
"fbjs/ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], "fbjs/ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="],
@@ -2215,10 +2214,6 @@
"metro-transform-worker/metro-source-map": ["metro-source-map@0.83.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.2", "nullthrows": "^1.1.1", "ob1": "0.83.2", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA=="], "metro-transform-worker/metro-source-map": ["metro-source-map@0.83.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.2", "nullthrows": "^1.1.1", "ob1": "0.83.2", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA=="],
"nativewind/@babel/types": ["@babel/types@7.19.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } }, "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA=="],
"nativewind/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"node-vibrant/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "node-vibrant/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"npm-package-arg/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], "npm-package-arg/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
@@ -2229,10 +2224,6 @@
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
@@ -2277,8 +2268,6 @@
"sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"tailwindcss/postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],

View File

@@ -132,15 +132,13 @@ export const DownloadItems: React.FC<DownloadProps> = ({
return itemsNotDownloaded.length === 0; return itemsNotDownloaded.length === 0;
}, [items, itemsNotDownloaded]); }, [items, itemsNotDownloaded]);
const itemsProcesses = useMemo( const itemsProcesses = useMemo(
() => () => processes?.filter((p) => itemIds.includes(p.item.Id)),
processes?.filter((p) => p?.item?.Id && itemIds.includes(p.item.Id)) ||
[],
[processes, itemIds], [processes, itemIds],
); );
const progress = useMemo(() => { const progress = useMemo(() => {
if (itemIds.length === 1) if (itemIds.length === 1)
return itemsProcesses.reduce((acc, p) => acc + (p.progress || 0), 0); return itemsProcesses.reduce((acc, p) => acc + p.progress, 0);
return ( return (
((itemIds.length - ((itemIds.length -
queue.filter((q) => itemIds.includes(q.item.Id)).length) / queue.filter((q) => itemIds.includes(q.item.Id)).length) /
@@ -264,9 +262,9 @@ export const DownloadItems: React.FC<DownloadProps> = ({
closeModal(); closeModal();
// Wait for modal dismiss animation to complete // Wait for modal dismiss animation to complete
setTimeout(() => { requestAnimationFrame(() => {
initiateDownload(...itemsToDownload); initiateDownload(...itemsToDownload);
}, 300); });
} else { } else {
toast.error( toast.error(
t("home.downloads.toasts.you_are_not_allowed_to_download_files"), t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
@@ -355,7 +353,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
keyboardBlurBehavior='restore' keyboardBlurBehavior='restore'
> >
<BottomSheetView> <BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col gap-y-4 px-4 pb-8 pt-2'>
<View> <View>
<Text className='font-bold text-2xl text-neutral-100'> <Text className='font-bold text-2xl text-neutral-100'>
{title} {title}
@@ -367,7 +365,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
})} })}
</Text> </Text>
</View> </View>
<View className='flex flex-col space-y-2 w-full'> <View className='flex flex-col gap-y-2 w-full'>
<View className='items-start'> <View className='items-start'>
<BitrateSelector <BitrateSelector
inverted inverted
@@ -406,7 +404,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
/> />
</View> </View>
{selectedOptions?.mediaSource && ( {selectedOptions?.mediaSource && (
<View className='flex flex-col space-y-2 items-start'> <View className='flex flex-col gap-y-2 items-start'>
<AudioTrackSelector <AudioTrackSelector
source={selectedOptions.mediaSource} source={selectedOptions.mediaSource}
onChange={(val) => { onChange={(val) => {

View File

@@ -12,7 +12,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { type Bitrate } from "@/components/BitrateSelector"; import { type Bitrate } from "@/components/BitrateSelector";
import { ItemImage } from "@/components/common/ItemImage"; import { ItemImage } from "@/components/common/ItemImage";
import { DownloadSingleItem } from "@/components/DownloadItem"; import { DownloadSingleItem } from "@/components/DownloadItem";
import { MediaSourceButton } from "@/components/MediaSourceButton";
import { OverviewText } from "@/components/OverviewText"; import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage"; import { ParallaxScrollView } from "@/components/ParallaxPage";
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null; // const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
@@ -24,16 +23,19 @@ import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel"; import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn"; import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
import { useItemQuery } from "@/hooks/useItemQuery";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { AddToFavorites } from "./AddToFavorites"; import { AddToFavorites } from "./AddToFavorites";
import { BitrateSheet } from "./BitRateSheet";
import { ItemHeader } from "./ItemHeader"; import { ItemHeader } from "./ItemHeader";
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { MediaSourceSheet } from "./MediaSourceSheet";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession"; import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
import { TrackSheet } from "./TrackSheet";
const Chromecast = !Platform.isTV ? require("./Chromecast") : null; const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
@@ -68,9 +70,6 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
SelectedOptions | undefined SelectedOptions | undefined
>(undefined); >(undefined);
// preload media sources
useItemQuery(item.Id, false, undefined, []);
const { const {
defaultAudioIndex, defaultAudioIndex,
defaultBitrate, defaultBitrate,
@@ -125,10 +124,10 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
)} )}
</View> </View>
) : ( ) : (
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
<Chromecast.Chromecast width={22} height={22} /> <Chromecast.Chromecast width={22} height={22} />
{item.Type !== "Program" && ( {item.Type !== "Program" && (
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
{!Platform.isTV && ( {!Platform.isTV && (
<DownloadSingleItem item={item} size='large' /> <DownloadSingleItem item={item} size='large' />
)} )}
@@ -202,27 +201,76 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
} }
> >
<View className='flex flex-col bg-transparent shrink'> <View className='flex flex-col bg-transparent shrink'>
<View className='flex flex-col px-4 w-full pt-2 mb-2 shrink'> <View className='flex flex-col px-4 w-full gap-y-2 pt-2 mb-2 shrink'>
<ItemHeader item={item} className='mb-2' /> <ItemHeader item={item} className='mb-2' />
{item.Type !== "Program" && !Platform.isTV && !isOffline && (
<View className='flex flex-row px-0 mb-2 justify-between space-x-2'> <View className='flex flex-row items-center justify-start w-full h-16 mb-2'>
<PlayButton <BitrateSheet
selectedOptions={selectedOptions} className='mr-1'
item={item} onChange={(val) =>
isOffline={isOffline} setSelectedOptions(
colors={itemColors} (prev) => prev && { ...prev, bitrate: val },
/> )
<View className='w-1' /> }
{!isOffline && ( selected={selectedOptions.bitrate}
<MediaSourceButton
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
item={item}
colors={itemColors}
/> />
)} <MediaSourceSheet
</View> className='mr-1'
item={item}
onChange={(val) =>
setSelectedOptions(
(prev) =>
prev && {
...prev,
mediaSource: val,
},
)
}
selected={selectedOptions.mediaSource}
/>
<TrackSheet
className='mr-1'
streamType='Audio'
title={t("item_card.audio")}
source={selectedOptions.mediaSource}
onChange={(val) => {
setSelectedOptions(
(prev) =>
prev && {
...prev,
audioIndex: val,
},
);
}}
selected={selectedOptions.audioIndex}
/>
<TrackSheet
source={selectedOptions.mediaSource}
streamType='Subtitle'
title={t("item_card.subtitles")}
onChange={(val) =>
setSelectedOptions(
(prev) =>
prev && {
...prev,
subtitleIndex: val,
},
)
}
selected={selectedOptions.subtitleIndex}
/>
</View>
)}
<PlayButton
className='grow'
selectedOptions={selectedOptions}
item={item}
isOffline={isOffline}
colors={itemColors}
/>
</View> </View>
{item.Type === "Episode" && ( {item.Type === "Episode" && (
<SeasonEpisodesCarousel <SeasonEpisodesCarousel
item={item} item={item}
@@ -231,6 +279,9 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
/> />
)} )}
{!isOffline && (
<ItemTechnicalDetails source={selectedOptions.mediaSource} />
)}
<OverviewText text={item.Overview} className='px-4 mb-4' /> <OverviewText text={item.Overview} className='px-4 mb-4' />
{item.Type !== "Program" && ( {item.Type !== "Program" && (

View File

@@ -15,7 +15,7 @@ export const ItemHeader: React.FC<Props> = ({ item, ...props }) => {
if (!item) if (!item)
return ( return (
<View <View
className='flex flex-col space-y-1.5 w-full items-start h-32' className='flex flex-col gap-y-1.5 w-full items-start h-32'
{...props} {...props}
> >
<View className='w-1/3 h-6 bg-neutral-900 rounded' /> <View className='w-1/3 h-6 bg-neutral-900 rounded' />

View File

@@ -29,7 +29,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
<View className='px-4 mt-2 mb-4'> <View className='px-4 mt-2 mb-4'>
<Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text> <Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text>
<TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}> <TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}>
<View className='flex flex-row space-x-2'> <View className='flex flex-row gap-x-2'>
<VideoStreamInfo source={source} /> <VideoStreamInfo source={source} />
</View> </View>
<Text className='text-purple-600'>{t("item_card.more_details")}</Text> <Text className='text-purple-600'>{t("item_card.more_details")}</Text>
@@ -52,12 +52,12 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
)} )}
> >
<BottomSheetScrollView> <BottomSheetScrollView>
<View className='flex flex-col space-y-2 p-4 mb-4'> <View className='flex flex-col gap-y-2 p-4 mb-4'>
<View> <View>
<Text className='text-lg font-bold mb-4'> <Text className='text-lg font-bold mb-4'>
{t("item_card.video")} {t("item_card.video")}
</Text> </Text>
<View className='flex flex-row space-x-2'> <View className='flex flex-row gap-x-2'>
<VideoStreamInfo source={source} /> <VideoStreamInfo source={source} />
</View> </View>
</View> </View>

View File

@@ -1,203 +0,0 @@
import { Ionicons } from "@expo/vector-icons";
import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
import { useItemQuery } from "@/hooks/useItemQuery";
import { BITRATES } from "./BitRateSheet";
import type { SelectedOptions } from "./ItemContent";
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
item: BaseItemDto;
selectedOptions: SelectedOptions;
setSelectedOptions: React.Dispatch<
React.SetStateAction<SelectedOptions | undefined>
>;
colors?: ThemeColors;
}
export const MediaSourceButton: React.FC<Props> = ({
item,
selectedOptions,
setSelectedOptions,
colors,
}: Props) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { data: itemWithSources, isLoading } = useItemQuery(
item.Id,
false,
undefined,
[],
);
const effectiveColors = colors || {
primary: "#7c3aed",
text: "#000000",
};
useEffect(() => {
const firstMediaSource = itemWithSources?.MediaSources?.[0];
if (!firstMediaSource) return;
setSelectedOptions((prev) => {
if (!prev) return prev;
return {
...prev,
mediaSource: firstMediaSource,
};
});
}, [itemWithSources, setSelectedOptions]);
const getMediaSourceDisplayName = useCallback((source: MediaSourceInfo) => {
const videoStream = source.MediaStreams?.find((x) => x.Type === "Video");
if (source.Name) return source.Name;
if (videoStream?.DisplayTitle) return videoStream.DisplayTitle;
return `Source ${source.Id}`;
}, []);
const audioStreams = useMemo(
() =>
selectedOptions.mediaSource?.MediaStreams?.filter(
(x) => x.Type === "Audio",
) || [],
[selectedOptions.mediaSource],
);
const subtitleStreams = useMemo(
() =>
selectedOptions.mediaSource?.MediaStreams?.filter(
(x) => x.Type === "Subtitle",
) || [],
[selectedOptions.mediaSource],
);
const optionGroups: OptionGroup[] = useMemo(() => {
const groups: OptionGroup[] = [];
// Bitrate group
groups.push({
title: t("item_card.quality"),
options: BITRATES.map((bitrate) => ({
type: "radio" as const,
label: bitrate.key,
value: bitrate,
selected: bitrate.value === selectedOptions.bitrate?.value,
onPress: () =>
setSelectedOptions((prev) => prev && { ...prev, bitrate }),
})),
});
// Media Source group (only if multiple sources)
if (
itemWithSources?.MediaSources &&
itemWithSources.MediaSources.length > 1
) {
groups.push({
title: t("item_card.video"),
options: itemWithSources.MediaSources.map((source) => ({
type: "radio" as const,
label: getMediaSourceDisplayName(source),
value: source,
selected: source.Id === selectedOptions.mediaSource?.Id,
onPress: () =>
setSelectedOptions(
(prev) => prev && { ...prev, mediaSource: source },
),
})),
});
}
// Audio track group
if (audioStreams.length > 0) {
groups.push({
title: t("item_card.audio"),
options: audioStreams.map((stream) => ({
type: "radio" as const,
label: stream.DisplayTitle || `${t("common.track")} ${stream.Index}`,
value: stream.Index,
selected: stream.Index === selectedOptions.audioIndex,
onPress: () =>
setSelectedOptions(
(prev) => prev && { ...prev, audioIndex: stream.Index ?? 0 },
),
})),
});
}
// Subtitle track group (with None option)
if (subtitleStreams.length > 0) {
const noneOption = {
type: "radio" as const,
label: t("common.none"),
value: -1,
selected: selectedOptions.subtitleIndex === -1,
onPress: () =>
setSelectedOptions((prev) => prev && { ...prev, subtitleIndex: -1 }),
};
const subtitleOptions = subtitleStreams.map((stream) => ({
type: "radio" as const,
label: stream.DisplayTitle || `${t("common.track")} ${stream.Index}`,
value: stream.Index,
selected: stream.Index === selectedOptions.subtitleIndex,
onPress: () =>
setSelectedOptions(
(prev) => prev && { ...prev, subtitleIndex: stream.Index ?? -1 },
),
}));
groups.push({
title: t("item_card.subtitles"),
options: [noneOption, ...subtitleOptions],
});
}
return groups;
}, [
itemWithSources,
selectedOptions,
audioStreams,
subtitleStreams,
getMediaSourceDisplayName,
t,
setSelectedOptions,
]);
const trigger = (
<TouchableOpacity
disabled={!item || isLoading}
onPress={() => setOpen(true)}
className='relative'
>
<View
style={{ backgroundColor: effectiveColors.primary, opacity: 0.7 }}
className='absolute w-12 h-12 rounded-full'
/>
<View className='w-12 h-12 rounded-full z-10 items-center justify-center'>
{isLoading ? (
<ActivityIndicator size='small' color={effectiveColors.text} />
) : (
<Ionicons name='list' size={24} color={effectiveColors.text} />
)}
</View>
</TouchableOpacity>
);
return (
<PlatformDropdown
groups={optionGroups}
trigger={trigger}
title={t("item_card.media_options")}
open={open}
onOpenChange={setOpen}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
);
};

View File

@@ -184,7 +184,7 @@ const PlatformDropdownComponent = ({
expoUIConfig, expoUIConfig,
bottomSheetConfig, bottomSheetConfig,
}: PlatformDropdownProps) => { }: PlatformDropdownProps) => {
const { showModal, hideModal, isVisible } = useGlobalModal(); const { showModal, hideModal } = useGlobalModal();
// Handle controlled open state for Android // Handle controlled open state for Android
useEffect(() => { useEffect(() => {
@@ -207,14 +207,6 @@ const PlatformDropdownComponent = ({
} }
}, [controlledOpen]); }, [controlledOpen]);
// Watch for modal dismissal on Android (e.g., swipe down, backdrop tap)
// and sync the controlled open state
useEffect(() => {
if (Platform.OS === "android" && controlledOpen === true && !isVisible) {
controlledOnOpenChange?.(false);
}
}, [isVisible, controlledOpen, controlledOnOpenChange]);
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
return ( return (
<Host style={expoUIConfig?.hostStyle}> <Host style={expoUIConfig?.hostStyle}>

View File

@@ -358,6 +358,9 @@ export const PlayButton: React.FC<Props> = ({
[startColor.value.text, endColor.value.text], [startColor.value.text, endColor.value.text],
), ),
})); }));
/**
* *********************
*/
// if (Platform.OS === "ios") // if (Platform.OS === "ios")
// return ( // return (
@@ -374,7 +377,7 @@ export const PlayButton: React.FC<Props> = ({
// color={effectiveColors.primary} // color={effectiveColors.primary}
// modifiers={[fixedSize()]} // modifiers={[fixedSize()]}
// > // >
// <View className='flex flex-row items-center space-x-2 h-full w-full justify-center -mb-3.5 '> // <View className='flex flex-row items-center gap-x-2 h-full w-full justify-center -mb-3.5 '>
// <Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}> // <Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
// {runtimeTicksToMinutes( // {runtimeTicksToMinutes(
// (item?.RunTimeTicks || 0) - // (item?.RunTimeTicks || 0) -
@@ -411,7 +414,7 @@ export const PlayButton: React.FC<Props> = ({
accessibilityLabel='Play button' accessibilityLabel='Play button'
accessibilityHint='Tap to play the media' accessibilityHint='Tap to play the media'
onPress={onPress} onPress={onPress}
className={"relative flex-1"} className={"relative"}
> >
<View className='absolute w-full h-full top-0 left-0 rounded-full z-10 overflow-hidden'> <View className='absolute w-full h-full top-0 left-0 rounded-full z-10 overflow-hidden'>
<Animated.View <Animated.View
@@ -437,7 +440,7 @@ export const PlayButton: React.FC<Props> = ({
}} }}
className='flex flex-row items-center justify-center bg-transparent rounded-full z-20 h-12 w-full ' className='flex flex-row items-center justify-center bg-transparent rounded-full z-20 h-12 w-full '
> >
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}> <Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
{runtimeTicksToMinutes( {runtimeTicksToMinutes(
(item?.RunTimeTicks || 0) - (item?.RunTimeTicks || 0) -

View File

@@ -200,7 +200,7 @@ export const PlayButton: React.FC<Props> = ({
}} }}
className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full ' className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full '
> >
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}> <Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
{runtimeTicksToMinutes(item?.RunTimeTicks)} {runtimeTicksToMinutes(item?.RunTimeTicks)}
</Animated.Text> </Animated.Text>

View File

@@ -21,7 +21,7 @@ interface Props extends ViewProps {
export const Ratings: React.FC<Props> = ({ item, ...props }) => { export const Ratings: React.FC<Props> = ({ item, ...props }) => {
if (!item) return null; if (!item) return null;
return ( return (
<View className='flex flex-row items-center mt-2 space-x-2' {...props}> <View className='flex flex-row items-center mt-2 gap-x-2' {...props}>
{item.OfficialRating && ( {item.OfficialRating && (
<Badge text={item.OfficialRating} variant='gray' /> <Badge text={item.OfficialRating} variant='gray' />
)} )}
@@ -79,7 +79,7 @@ export const JellyserrRatings: React.FC<{
!!result.voteCount || !!result.voteCount ||
(data?.criticsRating && !!data?.criticsScore) || (data?.criticsRating && !!data?.criticsScore) ||
(data?.audienceRating && !!data?.audienceScore)) && ( (data?.audienceRating && !!data?.audienceScore)) && (
<View className='flex flex-row flex-wrap space-x-1'> <View className='flex flex-row flex-wrap gap-x-1'>
{data?.criticsRating && !!data?.criticsScore && ( {data?.criticsRating && !!data?.criticsScore && (
<Badge <Badge
text={`${data.criticsScore}%`} text={`${data.criticsScore}%`}

View File

@@ -1,54 +0,0 @@
import { StyleSheet, Text, type TextProps } from "react-native";
export type ThemedTextProps = TextProps & {
lightColor?: string;
darkColor?: string;
type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link";
};
export function ThemedText({
style,
type = "default",
...rest
}: ThemedTextProps) {
return (
<Text
style={[
{ color: "white" },
type === "default" ? styles.default : undefined,
type === "title" ? styles.title : undefined,
type === "defaultSemiBold" ? styles.defaultSemiBold : undefined,
type === "subtitle" ? styles.subtitle : undefined,
type === "link" ? styles.link : undefined,
style,
]}
{...rest}
/>
);
}
const styles = StyleSheet.create({
default: {
fontSize: 16,
lineHeight: 24,
},
defaultSemiBold: {
fontSize: 16,
lineHeight: 24,
fontWeight: "600",
},
title: {
fontSize: 32,
fontWeight: "bold",
lineHeight: 32,
},
subtitle: {
fontSize: 20,
fontWeight: "bold",
},
link: {
lineHeight: 30,
fontSize: 16,
color: "#0a7ea4",
},
});

View File

@@ -1,15 +0,0 @@
import { View, type ViewProps } from "react-native";
export type ThemedViewProps = ViewProps & {
lightColor?: string;
darkColor?: string;
};
export function ThemedView({
style,
lightColor,
darkColor,
...otherProps
}: ThemedViewProps) {
return <View style={[{ backgroundColor: "black" }, style]} {...otherProps} />;
}

View File

@@ -16,10 +16,7 @@ export function Input(props: InputProps) {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
return Platform.isTV ? ( return Platform.isTV ? (
<TouchableOpacity <TouchableOpacity onFocus={() => inputRef?.current?.focus?.()}>
onPress={() => inputRef?.current?.focus?.()}
activeOpacity={1}
>
<TextInput <TextInput
ref={inputRef} ref={inputRef}
className={` className={`

View File

@@ -1,20 +1,16 @@
import { Platform, Text as RNText, type TextProps } from "react-native"; import { Platform, Text as RNText, type TextProps } from "react-native";
export function Text(props: TextProps) {
const { style, ...otherProps } = props; export function Text({ className, ...props }: TextProps) {
if (Platform.isTV) if (Platform.isTV)
return ( return (
<RNText <RNText allowFontScaling={false} style={{ color: "white" }} {...props} />
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}
/>
); );
return ( return (
<RNText <RNText
allowFontScaling={false} allowFontScaling={false}
style={[{ color: "white" }, style]} className={`text-white ${className}`}
{...otherProps} {...props}
/> />
); );
} }

View File

@@ -9,11 +9,7 @@ interface ActiveDownloadsProps extends ViewProps {}
export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
const { processes } = useDownload(); const { processes } = useDownload();
if (processes?.length === 0)
// Filter out any invalid processes before rendering
const validProcesses = processes?.filter((p) => p?.item?.Id) || [];
if (validProcesses.length === 0)
return ( return (
<View {...props} className='bg-neutral-900 p-4 rounded-2xl'> <View {...props} className='bg-neutral-900 p-4 rounded-2xl'>
<Text className='text-lg font-bold'> <Text className='text-lg font-bold'>
@@ -31,8 +27,8 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
{t("home.downloads.active_downloads")} {t("home.downloads.active_downloads")}
</Text> </Text>
<View className='gap-y-2'> <View className='gap-y-2'>
{validProcesses.map((p: JobStatus) => ( {processes?.map((p: JobStatus) => (
<DownloadCard key={p.id} process={p} /> <DownloadCard key={p.item.Id} process={p} />
))} ))}
</View> </View>
</View> </View>

View File

@@ -51,7 +51,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
}; };
const eta = useMemo(() => { const eta = useMemo(() => {
if (!process?.estimatedTotalSizeBytes || !process?.bytesDownloaded) { if (!process.estimatedTotalSizeBytes || !process.bytesDownloaded) {
return null; return null;
} }
@@ -66,14 +66,13 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
} }
return formatTimeString(secondsRemaining, "s"); return formatTimeString(secondsRemaining, "s");
}, [process?.id, process?.bytesDownloaded, process?.estimatedTotalSizeBytes]); }, [process.id, process.bytesDownloaded, process.estimatedTotalSizeBytes]);
const estimatedSize = useMemo(() => { const estimatedSize = useMemo(() => {
if (process?.estimatedTotalSizeBytes) if (process.estimatedTotalSizeBytes) return process.estimatedTotalSizeBytes;
return process.estimatedTotalSizeBytes;
// Calculate from bitrate + duration (only if bitrate value is defined) // Calculate from bitrate + duration (only if bitrate value is defined)
if (process?.maxBitrate?.value && process?.item?.RunTimeTicks) { if (process.maxBitrate.value) {
return estimateDownloadSize( return estimateDownloadSize(
process.maxBitrate.value, process.maxBitrate.value,
process.item.RunTimeTicks, process.item.RunTimeTicks,
@@ -82,43 +81,32 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
return undefined; return undefined;
}, [ }, [
process?.maxBitrate?.value, process.maxBitrate.value,
process?.item?.RunTimeTicks, process.item.RunTimeTicks,
process?.estimatedTotalSizeBytes, process.estimatedTotalSizeBytes,
]); ]);
const isTranscoding = process?.isTranscoding || false; const isTranscoding = process.isTranscoding || false;
const downloadedAmount = useMemo(() => { const downloadedAmount = useMemo(() => {
if (!process?.bytesDownloaded) return null; if (!process.bytesDownloaded) return null;
return formatBytes(process.bytesDownloaded); return formatBytes(process.bytesDownloaded);
}, [process?.bytesDownloaded]); }, [process.bytesDownloaded]);
const base64Image = useMemo(() => { const base64Image = useMemo(() => {
try { return storage.getString(process.item.Id!);
const itemId = process?.item?.Id; }, []);
if (!itemId) return undefined;
return storage.getString(itemId);
} catch {
return undefined;
}
}, [process?.item?.Id]);
// Sanitize progress to ensure it's within valid bounds // Sanitize progress to ensure it's within valid bounds
const sanitizedProgress = useMemo(() => { const sanitizedProgress = useMemo(() => {
if ( if (
typeof process?.progress !== "number" || typeof process.progress !== "number" ||
Number.isNaN(process.progress) Number.isNaN(process.progress)
) { ) {
return 0; return 0;
} }
return Math.max(0, Math.min(100, process.progress)); return Math.max(0, Math.min(100, process.progress));
}, [process?.progress]); }, [process.progress]);
// Return null after all hooks have been called
if (!process || !process.item || !process.item.Id) {
return null;
}
return ( return (
<TouchableOpacity <TouchableOpacity

View File

@@ -51,7 +51,7 @@ export const FilterButton = <T,>({
> >
<View <View
className={` className={`
px-3 py-1.5 rounded-full flex flex-row items-center space-x-1 px-3 py-1.5 rounded-full flex flex-row items-center gap-x-1
${ ${
values.length > 0 values.length > 0
? "bg-purple-600 border border-purple-700" ? "bg-purple-600 border border-purple-700"

View File

@@ -97,19 +97,20 @@ export const Home = () => {
} }
navigation.setOptions({ navigation.setOptions({
headerLeft: () => ( headerLeft: () => (
<TouchableOpacity <View className='flex flex-row items-center ml-1.5'>
onPress={() => { <TouchableOpacity
router.push("/(auth)/downloads"); onPress={() => {
}} router.push("/(auth)/downloads");
className='ml-1.5' }}
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
> >
<Feather <Feather
name='download' name='download'
color={hasDownloads ? Colors.primary : "white"} color={hasDownloads ? Colors.primary : "white"}
size={24} size={24}
/> />
</TouchableOpacity> </TouchableOpacity>
</View>
), ),
}); });
}, [navigation, router, hasDownloads]); }, [navigation, router, hasDownloads]);
@@ -470,8 +471,7 @@ export const Home = () => {
}} }}
> >
<View <View
className='flex flex-col space-y-4' className={`flex flex-col gap-y-4 ${Platform.OS === "android" ? "pt-4" : ""}`}
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
> >
{sections.map((section, index) => { {sections.map((section, index) => {
if (section.type === "InfiniteScrollingCollectionList") { if (section.type === "InfiniteScrollingCollectionList") {

View File

@@ -475,7 +475,7 @@ export const HomeWithCarousel = () => {
paddingTop: 0, paddingTop: 0,
}} }}
> >
<View className='flex flex-col space-y-4'> <View className='flex flex-col gap-y-4'>
{sections.map((section, index) => { {sections.map((section, index) => {
if (section.type === "InfiniteScrollingCollectionList") { if (section.type === "InfiniteScrollingCollectionList") {
return ( return (

View File

@@ -84,9 +84,9 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
return ( return (
<View {...props}> <View {...props}>
<Text className='px-4 text-lg font-bold mb-2 text-neutral-100'> <View className='px-4 mb-2'>
{title} <Text className='text-lg font-bold text-neutral-100'>{title}</Text>
</Text> </View>
{isLoading === false && allItems.length === 0 && ( {isLoading === false && allItems.length === 0 && (
<View className='px-4'> <View className='px-4'>
<Text className='text-neutral-500'>{t("home.no_items")}</Text> <Text className='text-neutral-500'>{t("home.no_items")}</Text>

View File

@@ -154,7 +154,7 @@ const DetailFacts: React.FC<
<Facts <Facts
title={t("jellyseerr.release_dates")} title={t("jellyseerr.release_dates")}
facts={filteredReleases?.map?.((r: Release, idx) => ( facts={filteredReleases?.map?.((r: Release, idx) => (
<View key={idx} className='flex flex-row space-x-2 items-center'> <View key={idx} className='flex flex-row gap-x-2 items-center'>
{r.type === 3 ? ( {r.type === 3 ? (
// Theatrical // Theatrical
<Ionicons name='ticket' size={16} color='white' /> <Ionicons name='ticket' size={16} color='white' />
@@ -189,7 +189,7 @@ const DetailFacts: React.FC<
<Facts <Facts
title={t("jellyseerr.production_country")} title={t("jellyseerr.production_country")}
facts={details?.productionCountries?.map((n, idx) => ( facts={details?.productionCountries?.map((n, idx) => (
<View key={idx} className='flex flex-row items-center space-x-2'> <View key={idx} className='flex flex-row items-center gap-x-2'>
<CountryFlag isoCode={n.iso_3166_1} size={10} /> <CountryFlag isoCode={n.iso_3166_1} size={10} />
<Text>{n.name}</Text> <Text>{n.name}</Text>
</View> </View>

View File

@@ -118,7 +118,7 @@ const ParallaxSlideShow = <T,>({
} }
logo={logo} logo={logo}
> >
<View className='flex flex-col space-y-4 px-4'> <View className='flex flex-col gap-y-4 px-4'>
<View className='flex flex-row justify-between w-full'> <View className='flex flex-row justify-between w-full'>
<View className='flex flex-col w-full'>{HeaderContent?.()}</View> <View className='flex flex-col w-full'>{HeaderContent?.()}</View>
</View> </View>

View File

@@ -144,14 +144,11 @@ const RequestModal = forwardRef<
}, [defaultServiceDetails]); }, [defaultServiceDetails]);
const seasonTitle = useMemo(() => { const seasonTitle = useMemo(() => {
if (!requestBody?.seasons || requestBody.seasons.length === 0) { if (requestBody?.seasons && requestBody?.seasons?.length > 1) {
return undefined;
}
if (requestBody.seasons.length > 1) {
return t("jellyseerr.season_all"); return t("jellyseerr.season_all");
} }
return t("jellyseerr.season_number", { return t("jellyseerr.season_number", {
season_number: requestBody.seasons[0], season_number: requestBody?.seasons,
}); });
}, [requestBody?.seasons]); }, [requestBody?.seasons]);
@@ -305,7 +302,7 @@ const RequestModal = forwardRef<
stackBehavior='push' stackBehavior='push'
> >
<BottomSheetView> <BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col gap-y-4 px-4 pb-8 pt-2'>
<View> <View>
<Text className='font-bold text-2xl text-neutral-100'> <Text className='font-bold text-2xl text-neutral-100'>
{t("jellyseerr.advanced")} {t("jellyseerr.advanced")}
@@ -314,7 +311,7 @@ const RequestModal = forwardRef<
<Text className='text-neutral-300'>{seasonTitle}</Text> <Text className='text-neutral-300'>{seasonTitle}</Text>
)} )}
</View> </View>
<View className='flex flex-col space-y-2'> <View className='flex flex-col gap-y-2'>
{defaultService && defaultServiceDetails && users && ( {defaultService && defaultServiceDetails && users && (
<> <>
<View className='flex flex-col'> <View className='flex flex-col'>

View File

@@ -31,7 +31,7 @@ const Discover: React.FC<Props> = ({ sliders }) => {
if (!hasSliders) return null; if (!hasSliders) return null;
return ( return (
<View className='flex flex-col space-y-4 mb-8'> <View className='flex flex-col gap-y-4 mb-8'>
{sortedSliders.map((slide) => { {sortedSliders.map((slide) => {
switch (slide.type) { switch (slide.type) {
case DiscoverSliderType.RECENT_REQUESTS: case DiscoverSliderType.RECENT_REQUESTS:

View File

@@ -83,7 +83,7 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
// Android UI // Android UI
return ( return (
<View className='flex flex-row justify-end items-center space-x-1'> <View className='flex flex-row justify-end items-center gap-x-1'>
<FilterButton <FilterButton
id={searchFilterId} id={searchFilterId}
queryKey='jellyseerr_search' queryKey='jellyseerr_search'

View File

@@ -32,7 +32,7 @@ export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
onPress={() => onPress={() =>
item?.SeriesId && router.push(`/series/${item.SeriesId}`) item?.SeriesId && router.push(`/series/${item.SeriesId}`)
} }
className='flex flex-col space-y-2 w-28' className='flex flex-col gap-y-2 w-28'
> >
<Poster <Poster
id={item?.Id} id={item?.Id}

View File

@@ -128,7 +128,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
}} }}
/> />
{episodes?.length ? ( {episodes?.length ? (
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
<DownloadItems <DownloadItems
title={t("item_card.download.download_season")} title={t("item_card.download.download_season")}
className='ml-2' className='ml-2'

View File

@@ -105,14 +105,14 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
android_keyboardInputMode='adjustResize' android_keyboardInputMode='adjustResize'
> >
<BottomSheetView> <BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col gap-y-4 px-4 pb-8 pt-2'>
<View> <View>
<Text className='font-bold text-2xl text-neutral-100'> <Text className='font-bold text-2xl text-neutral-100'>
{t("home.settings.quick_connect.quick_connect_title")} {t("home.settings.quick_connect.quick_connect_title")}
</Text> </Text>
</View> </View>
<View className='flex flex-col space-y-2'> <View className='flex flex-col gap-y-2'>
<View className='p-4 border border-neutral-800 rounded-xl bg-neutral-900 w-full space-y-4'> <View className='p-4 border border-neutral-800 rounded-xl bg-neutral-900 w-full gap-y-4'>
<Text className='text-neutral-400 text-center'> <Text className='text-neutral-400 text-center'>
{t( {t(
"home.settings.quick_connect.enter_the_quick_connect_code", "home.settings.quick_connect.enter_the_quick_connect_code",

View File

@@ -1,212 +0,0 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import { useQuery } from "@tanstack/react-query";
import { forwardRef, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Platform,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import type { StorageLocation } from "@/modules";
import { useSettings } from "@/utils/atoms/settings";
import {
clearStorageLocationsCache,
getAvailableStorageLocations,
} from "@/utils/storage";
interface StorageLocationPickerProps {
onClose: () => void;
}
export const StorageLocationPicker = forwardRef<
BottomSheetModal,
StorageLocationPickerProps
>(({ onClose }, ref) => {
const { t } = useTranslation();
const { settings, updateSettings } = useSettings();
const insets = useSafeAreaInsets();
const [selectedId, setSelectedId] = useState<string | undefined>(
settings.downloadStorageLocation || "internal",
);
const { data: locations, isLoading } = useQuery({
queryKey: ["storageLocations"],
queryFn: getAvailableStorageLocations,
enabled: Platform.OS === "android",
});
const handleSelect = (location: StorageLocation) => {
setSelectedId(location.id);
};
const handleConfirm = () => {
updateSettings({ downloadStorageLocation: selectedId });
clearStorageLocationsCache(); // Clear cache so next download uses new location
toast.success(
t("settings.storage.storage_location_updated", {
defaultValue: "Storage location updated",
}),
);
onClose();
};
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
if (Platform.OS !== "android") {
return null;
}
return (
<BottomSheetModal
ref={ref}
enableDynamicSizing
backdropComponent={renderBackdrop}
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
enablePanDownToClose
enableDismissOnClose
>
<BottomSheetScrollView
style={{
paddingLeft: Math.max(16, insets.left),
paddingRight: Math.max(16, insets.right),
}}
>
<View className='px-4 pt-2'>
<Text className='text-lg font-semibold mb-1'>
{t("settings.storage.select_storage_location", {
defaultValue: "Select Storage Location",
})}
</Text>
<Text className='text-sm text-neutral-500 mb-4'>
{t("settings.storage.existing_downloads_note", {
defaultValue:
"Existing downloads will remain in their current location",
})}
</Text>
{isLoading ? (
<View className='items-center justify-center py-8'>
<ActivityIndicator size='large' />
<Text className='mt-4 text-neutral-500'>
{t("settings.storage.loading_storage", {
defaultValue: "Loading storage options...",
})}
</Text>
</View>
) : !locations || locations.length === 0 ? (
<View className='items-center justify-center py-8'>
<Text className='text-neutral-500'>
{t("settings.storage.no_storage_found", {
defaultValue: "No storage locations found",
})}
</Text>
</View>
) : (
<>
{locations.map((location) => {
const isSelected = selectedId === location.id;
const freeSpaceGB = (location.freeSpace / 1024 ** 3).toFixed(2);
const totalSpaceGB = (location.totalSpace / 1024 ** 3).toFixed(
2,
);
const usedPercent = (
((location.totalSpace - location.freeSpace) /
location.totalSpace) *
100
).toFixed(0);
return (
<TouchableOpacity
key={location.id}
onPress={() => handleSelect(location)}
className={`p-4 mb-2 rounded-lg ${
isSelected
? "bg-purple-600/20 border border-purple-600"
: "bg-neutral-800"
}`}
>
<View className='flex-row items-center justify-between'>
<View className='flex-1'>
<View className='flex-row items-center'>
<Text className='text-base font-semibold'>
{location.label}
</Text>
{location.type === "external" && (
<View className='ml-2 px-2 py-0.5 bg-blue-600/30 rounded'>
<Text className='text-xs text-blue-400'>
{t("settings.storage.removable", {
defaultValue: "Removable",
})}
</Text>
</View>
)}
</View>
<Text className='text-sm text-neutral-500 mt-1'>
{t("settings.storage.space_info", {
defaultValue:
"{{free}} GB free of {{total}} GB ({{used}}% used)",
free: freeSpaceGB,
total: totalSpaceGB,
used: usedPercent,
})}
</Text>
</View>
{isSelected && (
<View className='w-6 h-6 rounded-full bg-purple-600 items-center justify-center ml-2'>
<Text className='text-white text-xs'></Text>
</View>
)}
</View>
</TouchableOpacity>
);
})}
<View className='flex-row gap-x-2 py-4'>
<TouchableOpacity
onPress={onClose}
className='flex-1 py-3 rounded-lg bg-neutral-800 items-center'
>
<Text className='text-white font-semibold'>
{t("common.cancel", { defaultValue: "Cancel" })}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleConfirm}
className='flex-1 py-3 rounded-lg bg-purple-600 items-center'
disabled={!selectedId}
>
<Text className='text-white font-semibold'>
{t("common.confirm", { defaultValue: "Confirm" })}
</Text>
</TouchableOpacity>
</View>
</>
)}
</View>
</BottomSheetScrollView>
</BottomSheetModal>
);
});

View File

@@ -1,6 +1,4 @@
import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
@@ -8,19 +6,14 @@ import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getStorageLabel } from "@/utils/storage";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
import { StorageLocationPicker } from "./StorageLocationPicker";
export const StorageSettings = () => { export const StorageSettings = () => {
const { deleteAllFiles, appSizeUsage } = useDownload(); const { deleteAllFiles, appSizeUsage } = useDownload();
const { settings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
const successHapticFeedback = useHaptic("success"); const successHapticFeedback = useHaptic("success");
const errorHapticFeedback = useHaptic("error"); const errorHapticFeedback = useHaptic("error");
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { data: size } = useQuery({ const { data: size } = useQuery({
queryKey: ["appSize"], queryKey: ["appSize"],
@@ -36,12 +29,6 @@ export const StorageSettings = () => {
}, },
}); });
const { data: storageLabel } = useQuery({
queryKey: ["storageLabel", settings.downloadStorageLocation],
queryFn: () => getStorageLabel(settings.downloadStorageLocation),
enabled: Platform.OS === "android",
});
const onDeleteClicked = async () => { const onDeleteClicked = async () => {
try { try {
await deleteAllFiles(); await deleteAllFiles();
@@ -115,32 +102,14 @@ export const StorageSettings = () => {
</View> </View>
</View> </View>
{!Platform.isTV && ( {!Platform.isTV && (
<> <ListGroup>
{Platform.OS === "android" && ( <ListItem
<ListGroup> textColor='red'
<ListItem onPress={onDeleteClicked}
title={t("settings.storage.download_location", { title={t("home.settings.storage.delete_all_downloaded_files")}
defaultValue: "Download Location", />
})} </ListGroup>
value={storageLabel || "Internal Storage"}
onPress={() => bottomSheetModalRef.current?.present()}
/>
</ListGroup>
)}
<ListGroup>
<ListItem
textColor='red'
onPress={onDeleteClicked}
title={t("home.settings.storage.delete_all_downloaded_files")}
/>
</ListGroup>
</>
)} )}
<StorageLocationPicker
ref={bottomSheetModalRef}
onClose={() => bottomSheetModalRef.current?.dismiss()}
/>
</View> </View>
); );
}; };

View File

@@ -130,7 +130,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
<Text className='text-xs opacity-50'>{item?.Album}</Text> <Text className='text-xs opacity-50'>{item?.Album}</Text>
)} )}
</View> </View>
<View className='flex flex-row space-x-2 shrink-0'> <View className='flex flex-row gap-x-2 shrink-0'>
<SkipButton <SkipButton
showButton={showSkipButton} showButton={showSkipButton}
onPress={skipIntro} onPress={skipIntro}

View File

@@ -127,7 +127,7 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
)} )}
</View> </View>
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center gap-x-2'>
{!Platform.isTV && {!Platform.isTV &&
(settings.defaultPlayer === VideoPlayer.VLC_4 || (settings.defaultPlayer === VideoPlayer.VLC_4 ||
Platform.OS === "android") && ( Platform.OS === "android") && (

View File

@@ -13,6 +13,12 @@ const SkipButton: React.FC<SkipButtonProps> = ({
buttonText, buttonText,
...props ...props
}) => { }) => {
console.log(`[SKIP_BUTTON] Render:`, {
buttonText,
showButton,
className: showButton ? "flex" : "hidden",
});
return ( return (
<View className={showButton ? "flex" : "hidden"} {...props}> <View className={showButton ? "flex" : "hidden"} {...props}>
<TouchableOpacity <TouchableOpacity

View File

@@ -130,13 +130,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
useEffect(() => { useEffect(() => {
const fetchTracks = async () => { const fetchTracks = async () => {
if (getSubtitleTracks) { if (getSubtitleTracks) {
let subtitleData: TrackInfo[] | null = null; let subtitleData = await getSubtitleTracks();
try {
subtitleData = await getSubtitleTracks();
} catch (error) {
console.log("[VideoContext] Failed to get subtitle tracks:", error);
return;
}
// Only FOR VLC 3, If we're transcoding, we need to reverse the subtitle data, because VLC reverses the HLS subtitles. // Only FOR VLC 3, If we're transcoding, we need to reverse the subtitle data, because VLC reverses the HLS subtitles.
if ( if (
mediaSource?.TranscodingUrl && mediaSource?.TranscodingUrl &&
@@ -185,13 +179,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
setSubtitleTracks(subtitles); setSubtitleTracks(subtitles);
} }
if (getAudioTracks) { if (getAudioTracks) {
let audioData: TrackInfo[] | null = null; const audioData = await getAudioTracks();
try {
audioData = await getAudioTracks();
} catch (error) {
console.log("[VideoContext] Failed to get audio tracks:", error);
return;
}
const allAudio = const allAudio =
mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
const audioTracks: Track[] = allAudio?.map((audio, idx) => { const audioTracks: Track[] = allAudio?.map((audio, idx) => {

View File

@@ -28,7 +28,6 @@ export const useGestureDetection = ({
onVerticalDragEnd, onVerticalDragEnd,
onTap, onTap,
screenWidth = 400, screenWidth = 400,
screenHeight = 800,
}: SwipeGestureOptions = {}) => { }: SwipeGestureOptions = {}) => {
const touchStartTime = useRef(0); const touchStartTime = useRef(0);
const touchStartPosition = useRef({ x: 0, y: 0 }); const touchStartPosition = useRef({ x: 0, y: 0 });
@@ -37,47 +36,25 @@ export const useGestureDetection = ({
const dragSide = useRef<"left" | "right" | null>(null); const dragSide = useRef<"left" | "right" | null>(null);
const hasMovedEnough = useRef(false); const hasMovedEnough = useRef(false);
const gestureType = useRef<"none" | "horizontal" | "vertical">("none"); const gestureType = useRef<"none" | "horizontal" | "vertical">("none");
const shouldIgnoreTouch = useRef(false);
const handleTouchStart = useCallback( const handleTouchStart = useCallback((event: GestureResponderEvent) => {
(event: GestureResponderEvent) => { touchStartTime.current = Date.now();
const startY = event.nativeEvent.pageY; touchStartPosition.current = {
x: event.nativeEvent.pageX,
// Define exclusion zones (15% from top and bottom) y: event.nativeEvent.pageY,
const topExclusionZone = screenHeight * 0.15; };
const bottomExclusionZone = screenHeight * 0.85; lastTouchPosition.current = {
x: event.nativeEvent.pageX,
// Check if touch started in exclusion zones y: event.nativeEvent.pageY,
if (startY < topExclusionZone || startY > bottomExclusionZone) { };
shouldIgnoreTouch.current = true; isDragging.current = false;
return; dragSide.current = null;
} hasMovedEnough.current = false;
gestureType.current = "none";
shouldIgnoreTouch.current = false; }, []);
touchStartTime.current = Date.now();
touchStartPosition.current = {
x: event.nativeEvent.pageX,
y: startY,
};
lastTouchPosition.current = {
x: event.nativeEvent.pageX,
y: startY,
};
isDragging.current = false;
dragSide.current = null;
hasMovedEnough.current = false;
gestureType.current = "none";
},
[screenHeight],
);
const handleTouchMove = useCallback( const handleTouchMove = useCallback(
(event: GestureResponderEvent) => { (event: GestureResponderEvent) => {
// Ignore touch if it started in exclusion zone
if (shouldIgnoreTouch.current) {
return;
}
const currentPosition = { const currentPosition = {
x: event.nativeEvent.pageX, x: event.nativeEvent.pageX,
y: event.nativeEvent.pageY, y: event.nativeEvent.pageY,
@@ -129,12 +106,6 @@ export const useGestureDetection = ({
const handleTouchEnd = useCallback( const handleTouchEnd = useCallback(
(event: GestureResponderEvent) => { (event: GestureResponderEvent) => {
// Ignore touch if it started in exclusion zone
if (shouldIgnoreTouch.current) {
shouldIgnoreTouch.current = false;
return;
}
const touchEndTime = Date.now(); const touchEndTime = Date.now();
const touchEndPosition = { const touchEndPosition = {
x: event.nativeEvent.pageX, x: event.nativeEvent.pageX,

View File

@@ -19,14 +19,10 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
useEffect(() => { useEffect(() => {
const fetchTracks = async () => { const fetchTracks = async () => {
if (playerRef.current) { if (playerRef.current) {
try { const audio = await playerRef.current.getAudioTracks();
const audio = await playerRef.current.getAudioTracks(); const subtitles = await playerRef.current.getSubtitleTracks();
const subtitles = await playerRef.current.getSubtitleTracks(); setAudioTracks(audio);
setAudioTracks(audio); setSubtitleTracks(subtitles);
setSubtitleTracks(subtitles);
} catch (error) {
console.log("[VideoDebugInfo] Failed to fetch tracks:", error);
}
} }
}; };
@@ -64,24 +60,8 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
className='mt-2.5 bg-blue-500 p-2 rounded' className='mt-2.5 bg-blue-500 p-2 rounded'
onPress={() => { onPress={() => {
if (playerRef.current) { if (playerRef.current) {
playerRef.current playerRef.current.getAudioTracks().then(setAudioTracks);
.getAudioTracks() playerRef.current.getSubtitleTracks().then(setSubtitleTracks);
.then(setAudioTracks)
.catch((err) => {
console.log(
"[VideoDebugInfo] Failed to get audio tracks:",
err,
);
});
playerRef.current
.getSubtitleTracks()
.then(setSubtitleTracks)
.catch((err) => {
console.log(
"[VideoDebugInfo] Failed to get subtitle tracks:",
err,
);
});
} }
}} }}
> >

View File

@@ -45,14 +45,14 @@
}, },
"production": { "production": {
"environment": "production", "environment": "production",
"channel": "0.48.0", "channel": "0.47.1",
"android": { "android": {
"image": "latest" "image": "latest"
} }
}, },
"production-apk": { "production-apk": {
"environment": "production", "environment": "production",
"channel": "0.48.0", "channel": "0.47.1",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"
@@ -60,7 +60,7 @@
}, },
"production-apk-tv": { "production-apk-tv": {
"environment": "production", "environment": "production",
"channel": "0.48.0", "channel": "0.47.1",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"

5
global.css Normal file
View File

@@ -0,0 +1,5 @@
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";
@import "nativewind/theme";

View File

@@ -2,14 +2,8 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import type * as ImageColorsType from "react-native-image-colors"; import { getColors, ImageColorsResult } from "react-native-image-colors";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
// Conditionally import react-native-image-colors only on non-TV platforms
const ImageColors = Platform.isTV
? null
: (require("react-native-image-colors") as typeof ImageColorsType);
import { import {
adjustToNearBlack, adjustToNearBlack,
calculateTextColor, calculateTextColor,
@@ -70,13 +64,11 @@ export const useImageColors = ({
} }
// Extract colors from the image // Extract colors from the image
if (!ImageColors?.getColors) return; getColors(source.uri, {
ImageColors.getColors(source.uri, {
fallback: "#fff", fallback: "#fff",
cache: false, cache: false,
}) })
.then((colors: ImageColorsType.ImageColorsResult) => { .then((colors: ImageColorsResult) => {
let primary = "#fff"; let primary = "#fff";
let text = "#000"; let text = "#000";
let backup = "#fff"; let backup = "#fff";

View File

@@ -2,14 +2,8 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import type * as ImageColorsType from "react-native-image-colors"; import { getColors, ImageColorsResult } from "react-native-image-colors";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
// Conditionally import react-native-image-colors only on non-TV platforms
const ImageColors = Platform.isTV
? null
: (require("react-native-image-colors") as typeof ImageColorsType);
import { import {
adjustToNearBlack, adjustToNearBlack,
calculateTextColor, calculateTextColor,
@@ -86,13 +80,11 @@ export const useImageColorsReturn = ({
} }
// Extract colors from the image // Extract colors from the image
if (!ImageColors?.getColors) return; getColors(source.uri, {
ImageColors.getColors(source.uri, {
fallback: "#fff", fallback: "#fff",
cache: false, cache: false,
}) })
.then((colors: ImageColorsType.ImageColorsResult) => { .then((colors: ImageColorsResult) => {
let primary = "#fff"; let primary = "#fff";
let text = "#000"; let text = "#000";
let backup = "#fff"; let backup = "#fff";

View File

@@ -43,14 +43,37 @@ export const useIntroSkipper = (
const introTimestamps = segments?.introSegments?.[0]; const introTimestamps = segments?.introSegments?.[0];
useEffect(() => { useEffect(() => {
console.log(`[INTRO_SKIPPER] Hook state:`, {
itemId,
currentTime,
hasSegments: !!segments,
segments: segments,
introSegmentsCount: segments?.introSegments?.length || 0,
introSegments: segments?.introSegments,
hasIntroTimestamps: !!introTimestamps,
introTimestamps,
isVlc,
isOffline,
});
if (introTimestamps) { if (introTimestamps) {
const shouldShow = const shouldShow =
currentTime > introTimestamps.startTime && currentTime > introTimestamps.startTime &&
currentTime < introTimestamps.endTime; currentTime < introTimestamps.endTime;
console.log(`[INTRO_SKIPPER] Button visibility check:`, {
currentTime,
introStart: introTimestamps.startTime,
introEnd: introTimestamps.endTime,
afterStart: currentTime > introTimestamps.startTime,
beforeEnd: currentTime < introTimestamps.endTime,
shouldShow,
});
setShowSkipButton(shouldShow); setShowSkipButton(shouldShow);
} else { } else {
if (showSkipButton) { if (showSkipButton) {
console.log(`[INTRO_SKIPPER] No intro timestamps, hiding button`);
setShowSkipButton(false); setShowSkipButton(false);
} }
} }
@@ -59,6 +82,10 @@ export const useIntroSkipper = (
const skipIntro = useCallback(() => { const skipIntro = useCallback(() => {
if (!introTimestamps) return; if (!introTimestamps) return;
try { try {
console.log(
`[INTRO_SKIPPER] Skipping intro to:`,
introTimestamps.endTime,
);
lightHapticFeedback(); lightHapticFeedback();
wrappedSeek(introTimestamps.endTime); wrappedSeek(introTimestamps.endTime);
setTimeout(() => { setTimeout(() => {
@@ -69,5 +96,7 @@ export const useIntroSkipper = (
} }
}, [introTimestamps, lightHapticFeedback, wrappedSeek, play]); }, [introTimestamps, lightHapticFeedback, wrappedSeek, play]);
console.log(`[INTRO_SKIPPER] Returning state:`, { showSkipButton });
return { showSkipButton, skipIntro }; return { showSkipButton, skipIntro };
}; };

View File

@@ -1,54 +1,28 @@
import { ItemFields } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
// Helper to exclude specific fields export const useItemQuery = (itemId: string, isOffline: boolean) => {
export const excludeFields = (fieldsToExclude: ItemFields[]) => {
return Object.values(ItemFields).filter(
(field) => !fieldsToExclude.includes(field),
);
};
export const useItemQuery = (
itemId: string | undefined,
isOffline?: boolean,
fields?: ItemFields[],
excludeFields?: ItemFields[],
) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { getDownloadedItemById } = useDownload(); const { getDownloadedItemById } = useDownload();
// Calculate final fields: use excludeFields if provided, otherwise use fields
const finalFields = excludeFields
? Object.values(ItemFields).filter(
(field) => !excludeFields.includes(field),
)
: fields;
return useQuery({ return useQuery({
queryKey: ["item", itemId, finalFields], queryKey: ["item", itemId],
queryFn: async () => { queryFn: async () => {
if (!itemId) throw new Error("Item ID is required");
if (isOffline) { if (isOffline) {
return getDownloadedItemById(itemId)?.item; return getDownloadedItemById(itemId)?.item;
} }
if (!api || !user || !itemId) return null;
if (!api || !user) return null; const res = await getUserLibraryApi(api).getItem({
itemId: itemId,
const response = await getItemsApi(api).getItems({ userId: user?.Id,
ids: [itemId],
userId: user.Id,
...(finalFields && { fields: finalFields }),
}); });
return res.data;
return response.data.Items?.[0];
}, },
enabled: !!itemId, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchOnReconnect: true, refetchOnReconnect: true,

View File

@@ -512,7 +512,7 @@ export const useJellyseerr = () => {
}; };
const jellyseerrRegion = useMemo( const jellyseerrRegion = useMemo(
() => jellyseerrUser?.settings?.region || "US", () => jellyseerrUser?.settings?.discoverRegion || "US",
[jellyseerrUser], [jellyseerrUser],
); );

View File

@@ -1,19 +1,12 @@
import type { OrientationChangeEvent } from "expo-screen-orientation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { import * as ScreenOrientation from "@/packages/expo-screen-orientation";
addOrientationChangeListener, import { OrientationLock } from "@/packages/expo-screen-orientation";
getOrientationAsync,
lockAsync,
Orientation as OrientationEnum,
OrientationLock,
unlockAsync,
} from "@/packages/expo-screen-orientation";
import { Orientation } from "../packages/expo-screen-orientation.tv"; import { Orientation } from "../packages/expo-screen-orientation.tv";
const orientationToOrientationLock = ( const orientationToOrientationLock = (
orientation: (typeof OrientationEnum)[keyof typeof OrientationEnum], orientation: Orientation,
): (typeof OrientationLock)[keyof typeof OrientationLock] => { ): OrientationLock => {
switch (orientation) { switch (orientation) {
case Orientation.LANDSCAPE_LEFT: case Orientation.LANDSCAPE_LEFT:
return OrientationLock.LANDSCAPE_LEFT; return OrientationLock.LANDSCAPE_LEFT;
@@ -28,52 +21,44 @@ const orientationToOrientationLock = (
export const useOrientation = () => { export const useOrientation = () => {
const [orientation, setOrientation] = useState( const [orientation, setOrientation] = useState(
Platform.isTV ? OrientationLock.LANDSCAPE : OrientationLock.UNKNOWN, Platform.isTV
? ScreenOrientation.OrientationLock.LANDSCAPE
: ScreenOrientation.OrientationLock.UNKNOWN,
); );
useEffect(() => { useEffect(() => {
if (Platform.isTV) return; if (Platform.isTV) return;
const orientationSubscription = addOrientationChangeListener( const orientationSubscription =
(event: OrientationChangeEvent) => { ScreenOrientation.addOrientationChangeListener((event) => {
setOrientation( setOrientation(
orientationToOrientationLock(event.orientationInfo.orientation), orientationToOrientationLock(event.orientationInfo.orientation),
); );
}, });
);
getOrientationAsync().then( ScreenOrientation.getOrientationAsync().then((orientation) => {
(orientation: (typeof OrientationEnum)[keyof typeof OrientationEnum]) => { setOrientation(orientationToOrientationLock(orientation));
setOrientation(orientationToOrientationLock(orientation)); });
},
);
return () => { return () => {
orientationSubscription.remove(); orientationSubscription.remove();
}; };
}, []); }, []);
const lockOrientation = async ( const lockOrientation = async (lock: OrientationLock) => {
lock: (typeof OrientationLock)[keyof typeof OrientationLock],
) => {
if (Platform.isTV) return; if (Platform.isTV) return;
if (lock === OrientationLock.DEFAULT) { if (lock === ScreenOrientation.OrientationLock.DEFAULT) {
await unlockAsync(); await ScreenOrientation.unlockAsync();
} else { } else {
await lockAsync(lock); await ScreenOrientation.lockAsync(lock);
} }
}; };
const unlockOrientationFn = async () => { const unlockOrientation = async () => {
if (Platform.isTV) return; if (Platform.isTV) return;
await unlockAsync(); await ScreenOrientation.unlockAsync();
}; };
return { return { orientation, setOrientation, lockOrientation, unlockOrientation };
orientation,
setOrientation,
lockOrientation,
unlockOrientation: unlockOrientationFn,
};
}; };

View File

@@ -1,5 +1,6 @@
// Learn more https://docs.expo.io/guides/customizing-metro // Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config"); const { getDefaultConfig } = require("expo/metro-config");
const { withNativewind } = require("nativewind/metro");
/** @type {import('expo/metro-config').MetroConfig} */ /** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname); const config = getDefaultConfig(__dirname);
@@ -25,4 +26,4 @@ if (process.env?.EXPO_TV === "1") {
// config.resolver.unstable_enablePackageExports = false; // config.resolver.unstable_enablePackageExports = false;
module.exports = config; module.exports = withNativewind(config);

View File

@@ -59,13 +59,6 @@ export type ChapterInfo = {
duration: number; duration: number;
}; };
export type NowPlayingMetadata = {
title?: string;
artist?: string;
albumTitle?: string;
artworkUri?: string;
};
export type VlcPlayerViewProps = { export type VlcPlayerViewProps = {
source: VlcPlayerSource; source: VlcPlayerSource;
style?: ViewStyle | ViewStyle[]; style?: ViewStyle | ViewStyle[];
@@ -74,7 +67,6 @@ export type VlcPlayerViewProps = {
muted?: boolean; muted?: boolean;
volume?: number; volume?: number;
videoAspectRatio?: string; videoAspectRatio?: string;
nowPlayingMetadata?: NowPlayingMetadata;
onVideoProgress?: (event: ProgressUpdatePayload) => void; onVideoProgress?: (event: ProgressUpdatePayload) => void;
onVideoStateChange?: (event: PlaybackStatePayload) => void; onVideoStateChange?: (event: PlaybackStatePayload) => void;
onVideoLoadStart?: (event: VideoLoadStartPayload) => void; onVideoLoadStart?: (event: VideoLoadStartPayload) => void;

View File

@@ -102,7 +102,6 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
muted, muted,
volume, volume,
videoAspectRatio, videoAspectRatio,
nowPlayingMetadata,
onVideoLoadStart, onVideoLoadStart,
onVideoStateChange, onVideoStateChange,
onVideoProgress, onVideoProgress,
@@ -132,7 +131,6 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
muted={muted} muted={muted}
volume={volume} volume={volume}
videoAspectRatio={videoAspectRatio} videoAspectRatio={videoAspectRatio}
nowPlayingMetadata={nowPlayingMetadata}
onVideoLoadStart={onVideoLoadStart} onVideoLoadStart={onVideoLoadStart}
onVideoLoadEnd={onVideoLoadEnd} onVideoLoadEnd={onVideoLoadEnd}
onVideoStateChange={onVideoStateChange} onVideoStateChange={onVideoStateChange}

View File

@@ -35,7 +35,7 @@ android {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "com.squareup.okhttp3:okhttp:5.3.0" implementation "com.squareup.okhttp3:okhttp:4.12.0"
} }
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {

View File

@@ -4,16 +4,11 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Build
import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.util.Log import android.util.Log
import expo.modules.kotlin.Promise import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.modules.ModuleDefinition
import java.io.File
data class DownloadTaskInfo( data class DownloadTaskInfo(
val url: String, val url: String,
@@ -147,105 +142,6 @@ class BackgroundDownloaderModule : Module() {
promise.reject("ERROR", "Failed to get active downloads: ${e.message}", e) promise.reject("ERROR", "Failed to get active downloads: ${e.message}", e)
} }
} }
AsyncFunction("getAvailableStorageLocations") { promise: Promise ->
try {
val storageLocations = mutableListOf<Map<String, Any>>()
// Use getExternalFilesDirs which works reliably across all Android versions
// This returns app-specific directories on both internal and external storage
val externalDirs = context.getExternalFilesDirs(null)
Log.d(TAG, "getExternalFilesDirs returned ${externalDirs.size} locations")
// Also check with StorageManager for additional info
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val volumes = storageManager.storageVolumes
Log.d(TAG, "StorageManager reports ${volumes.size} volumes")
for ((i, vol) in volumes.withIndex()) {
Log.d(TAG, " Volume $i: isPrimary=${vol.isPrimary}, isRemovable=${vol.isRemovable}, state=${vol.state}, uuid=${vol.uuid}")
}
}
for ((index, dir) in externalDirs.withIndex()) {
try {
if (dir == null) {
Log.w(TAG, "Directory at index $index is null - SD card may not be mounted")
continue
}
if (!dir.exists()) {
Log.w(TAG, "Directory at index $index does not exist: ${dir.absolutePath}")
continue
}
val isPrimary = index == 0
val isRemovable = !isPrimary && Environment.isExternalStorageRemovable(dir)
// Get volume UUID for better identification
val volumeId = if (isPrimary) {
"internal"
} else {
// Try to get a stable UUID for the SD card
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
try {
val storageVolume = storageManager.getStorageVolume(dir)
storageVolume?.uuid ?: "sdcard_$index"
} catch (e: Exception) {
"sdcard_$index"
}
} else {
"sdcard_$index"
}
}
// Get human-readable label
val label = if (isPrimary) {
"Internal Storage"
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
try {
val storageVolume = storageManager.getStorageVolume(dir)
storageVolume?.getDescription(context) ?: "SD Card"
} catch (e: Exception) {
"SD Card"
}
} else {
"SD Card"
}
}
val totalSpace = dir.totalSpace
val freeSpace = dir.freeSpace
Log.d(TAG, "Storage location: $label (id: $volumeId, path: ${dir.absolutePath}, removable: $isRemovable)")
storageLocations.add(
mapOf(
"id" to volumeId,
"path" to dir.absolutePath,
"type" to (if (isRemovable || !isPrimary) "external" else "internal"),
"label" to label,
"totalSpace" to totalSpace,
"freeSpace" to freeSpace
)
)
} catch (e: Exception) {
Log.e(TAG, "Error processing storage at index $index: ${e.message}", e)
continue
}
}
Log.d(TAG, "Returning ${storageLocations.size} storage locations")
promise.resolve(storageLocations)
} catch (e: Exception) {
Log.e(TAG, "Error getting storage locations: ${e.message}", e)
promise.reject("ERROR", "Failed to get storage locations: ${e.message}", e)
}
}
} }
private fun startDownloadInternal(urlString: String, destinationPath: String?): Int { private fun startDownloadInternal(urlString: String, destinationPath: String?): Int {

View File

@@ -5,7 +5,6 @@ import type {
DownloadErrorEvent, DownloadErrorEvent,
DownloadProgressEvent, DownloadProgressEvent,
DownloadStartedEvent, DownloadStartedEvent,
StorageLocation,
} from "./src/BackgroundDownloader.types"; } from "./src/BackgroundDownloader.types";
import BackgroundDownloaderModule from "./src/BackgroundDownloaderModule"; import BackgroundDownloaderModule from "./src/BackgroundDownloaderModule";
@@ -16,7 +15,6 @@ export interface BackgroundDownloader {
cancelQueuedDownload(url: string): void; cancelQueuedDownload(url: string): void;
cancelAllDownloads(): void; cancelAllDownloads(): void;
getActiveDownloads(): Promise<ActiveDownload[]>; getActiveDownloads(): Promise<ActiveDownload[]>;
getAvailableStorageLocations(): Promise<StorageLocation[]>;
addProgressListener( addProgressListener(
listener: (event: DownloadProgressEvent) => void, listener: (event: DownloadProgressEvent) => void,
@@ -66,10 +64,6 @@ const BackgroundDownloader: BackgroundDownloader = {
return await BackgroundDownloaderModule.getActiveDownloads(); return await BackgroundDownloaderModule.getActiveDownloads();
}, },
async getAvailableStorageLocations(): Promise<StorageLocation[]> {
return await BackgroundDownloaderModule.getAvailableStorageLocations();
},
addProgressListener( addProgressListener(
listener: (event: DownloadProgressEvent) => void, listener: (event: DownloadProgressEvent) => void,
): EventSubscription { ): EventSubscription {
@@ -112,5 +106,4 @@ export type {
DownloadErrorEvent, DownloadErrorEvent,
DownloadProgressEvent, DownloadProgressEvent,
DownloadStartedEvent, DownloadStartedEvent,
StorageLocation,
}; };

View File

@@ -29,15 +29,6 @@ export interface ActiveDownload {
state: "running" | "suspended" | "canceling" | "completed" | "unknown"; state: "running" | "suspended" | "canceling" | "completed" | "unknown";
} }
export interface StorageLocation {
id: string;
path: string;
type: "internal" | "external";
label: string;
totalSpace: number;
freeSpace: number;
}
export interface BackgroundDownloaderModuleType { export interface BackgroundDownloaderModuleType {
startDownload(url: string, destinationPath?: string): Promise<number>; startDownload(url: string, destinationPath?: string): Promise<number>;
enqueueDownload(url: string, destinationPath?: string): Promise<number>; enqueueDownload(url: string, destinationPath?: string): Promise<number>;
@@ -45,7 +36,6 @@ export interface BackgroundDownloaderModuleType {
cancelQueuedDownload(url: string): void; cancelQueuedDownload(url: string): void;
cancelAllDownloads(): void; cancelAllDownloads(): void;
getActiveDownloads(): Promise<ActiveDownload[]>; getActiveDownloads(): Promise<ActiveDownload[]>;
getAvailableStorageLocations(): Promise<StorageLocation[]>;
addListener( addListener(
eventName: string, eventName: string,
listener: (event: any) => void, listener: (event: any) => void,

View File

@@ -18,7 +18,6 @@ export type {
DownloadErrorEvent, DownloadErrorEvent,
DownloadProgressEvent, DownloadProgressEvent,
DownloadStartedEvent, DownloadStartedEvent,
StorageLocation,
} from "./background-downloader"; } from "./background-downloader";
// Background Downloader // Background Downloader
export { default as BackgroundDownloader } from "./background-downloader"; export { default as BackgroundDownloader } from "./background-downloader";

View File

@@ -16,12 +16,6 @@ public class VlcPlayerModule: Module {
} }
} }
Prop("nowPlayingMetadata") { (view: VlcPlayerView, metadata: [String: String]?) in
if let metadata = metadata {
view.setNowPlayingMetadata(metadata)
}
}
Events( Events(
"onPlaybackStateChanged", "onPlaybackStateChanged",
"onVideoStateChange", "onVideoStateChange",

View File

@@ -1,6 +1,4 @@
import ExpoModulesCore import ExpoModulesCore
import MediaPlayer
import AVFoundation
#if os(tvOS) #if os(tvOS)
import TVVLCKit import TVVLCKit
@@ -26,9 +24,6 @@ class VlcPlayerView: ExpoView {
var hasSource = false var hasSource = false
var isTranscoding = false var isTranscoding = false
private var initialSeekPerformed: Bool = false private var initialSeekPerformed: Bool = false
private var nowPlayingMetadata: [String: String]?
private var artworkImage: UIImage?
private var artworkDownloadTask: URLSessionDataTask?
// MARK: - Initialization // MARK: - Initialization
@@ -36,8 +31,6 @@ class VlcPlayerView: ExpoView {
super.init(appContext: appContext) super.init(appContext: appContext)
setupView() setupView()
setupNotifications() setupNotifications()
setupRemoteCommandCenter()
setupAudioSession()
} }
// MARK: - Setup // MARK: - Setup
@@ -67,205 +60,42 @@ class VlcPlayerView: ExpoView {
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, selector: #selector(applicationDidBecomeActive), self, selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification, object: nil) name: UIApplication.didBecomeActiveNotification, object: nil)
#if !os(tvOS)
// Handle audio session interruptions (e.g., incoming calls, other apps playing audio)
NotificationCenter.default.addObserver(
self, selector: #selector(handleAudioSessionInterruption),
name: AVAudioSession.interruptionNotification, object: nil)
#endif
}
private func setupAudioSession() {
#if !os(tvOS)
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback, options: [])
try audioSession.setActive(true)
print("Audio session configured for media controls")
} catch {
print("Failed to setup audio session: \(error)")
}
#endif
}
private func setupRemoteCommandCenter() {
#if !os(tvOS)
let commandCenter = MPRemoteCommandCenter.shared()
// Play command
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [weak self] _ in
self?.play()
return .success
}
// Pause command
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { [weak self] _ in
self?.pause()
return .success
}
// Toggle play/pause command
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
guard let self = self, let player = self.mediaPlayer else {
return .commandFailed
}
if player.isPlaying {
self.pause()
} else {
self.play()
}
return .success
}
// Seek forward command
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.preferredIntervals = [15]
commandCenter.skipForwardCommand.addTarget { [weak self] event in
guard let self = self, let player = self.mediaPlayer else {
return .commandFailed
}
let skipInterval = (event as? MPSkipIntervalCommandEvent)?.interval ?? 15
let currentTime = player.time.intValue
self.seekTo(currentTime + Int32(skipInterval * 1000))
return .success
}
// Seek backward command
commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [15]
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
guard let self = self, let player = self.mediaPlayer else {
return .commandFailed
}
let skipInterval = (event as? MPSkipIntervalCommandEvent)?.interval ?? 15
let currentTime = player.time.intValue
self.seekTo(max(0, currentTime - Int32(skipInterval * 1000)))
return .success
}
// Change playback position command (scrubbing)
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
guard let self = self,
let event = event as? MPChangePlaybackPositionCommandEvent else {
return .commandFailed
}
let positionTime = event.positionTime
self.seekTo(Int32(positionTime * 1000))
return .success
}
print("Remote command center configured")
#endif
}
private func cleanupRemoteCommandCenter() {
#if !os(tvOS)
let commandCenter = MPRemoteCommandCenter.shared()
// Remove all command targets to prevent memory leaks
commandCenter.playCommand.removeTarget(nil)
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.togglePlayPauseCommand.removeTarget(nil)
commandCenter.skipForwardCommand.removeTarget(nil)
commandCenter.skipBackwardCommand.removeTarget(nil)
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
// Disable commands
commandCenter.playCommand.isEnabled = false
commandCenter.pauseCommand.isEnabled = false
commandCenter.togglePlayPauseCommand.isEnabled = false
commandCenter.skipForwardCommand.isEnabled = false
commandCenter.skipBackwardCommand.isEnabled = false
commandCenter.changePlaybackPositionCommand.isEnabled = false
print("Remote command center cleaned up")
#endif
} }
// MARK: - Public Methods // MARK: - Public Methods
func startPictureInPicture() {} func startPictureInPicture() {}
@objc func play() { @objc func play() {
DispatchQueue.main.async { self.mediaPlayer?.play()
self.mediaPlayer?.play() self.isPaused = false
self.isPaused = false print("Play")
self.updateNowPlayingInfo()
print("Play")
}
} }
@objc func pause() { @objc func pause() {
DispatchQueue.main.async { self.mediaPlayer?.pause()
self.mediaPlayer?.pause() self.isPaused = true
self.isPaused = true
self.updateNowPlayingInfo()
}
}
@objc func handleAudioSessionInterruption(_ notification: Notification) {
#if !os(tvOS)
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
switch type {
case .began:
// Interruption began - pause the video
print("Audio session interrupted - pausing video")
self.pause()
case .ended:
// Interruption ended - check if we should resume
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
print("Audio session interruption ended - can resume")
// Don't auto-resume - let user manually resume playback
} else {
print("Audio session interruption ended - should not resume")
}
}
@unknown default:
break
}
#endif
} }
@objc func seekTo(_ time: Int32) { @objc func seekTo(_ time: Int32) {
DispatchQueue.main.async { guard let player = self.mediaPlayer else { return }
guard let player = self.mediaPlayer else { return }
let wasPlaying = player.isPlaying let wasPlaying = player.isPlaying
if wasPlaying {
self.pause()
}
if let duration = player.media?.length.intValue {
print("Seeking to time: \(time) Video Duration \(duration)")
// If the specified time is greater than the duration, seek to the end
let seekTime = time > duration ? duration - 1000 : time
player.time = VLCTime(int: seekTime)
if wasPlaying { if wasPlaying {
player.pause() self.play()
}
if let duration = player.media?.length.intValue {
print("Seeking to time: \(time) Video Duration \(duration)")
// If the specified time is greater than the duration, seek to the end
let seekTime = time > duration ? duration - 1000 : time
player.time = VLCTime(int: seekTime)
if wasPlaying {
player.play()
}
self.updatePlayerState()
self.updateNowPlayingInfo()
} else {
print("Error: Unable to retrieve video duration")
} }
self.updatePlayerState()
} else {
print("Error: Unable to retrieve video duration")
} }
} }
@@ -433,55 +263,6 @@ class VlcPlayerView: ExpoView {
} }
} }
@objc func setNowPlayingMetadata(_ metadata: [String: String]) {
// Cancel any existing artwork download to prevent race conditions
artworkDownloadTask?.cancel()
artworkDownloadTask = nil
self.nowPlayingMetadata = metadata
print("[NowPlaying] Metadata received: \(metadata)")
// Load artwork asynchronously if provided
if let artworkUri = metadata["artworkUri"], let url = URL(string: artworkUri) {
print("[NowPlaying] Loading artwork from: \(artworkUri)")
artworkDownloadTask = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error as NSError?, error.code == NSURLErrorCancelled {
print("[NowPlaying] Artwork download cancelled")
return
}
if let error = error {
print("[NowPlaying] Artwork loading error: \(error)")
DispatchQueue.main.async {
self.updateNowPlayingInfo()
}
} else if let data = data, let image = UIImage(data: data) {
print("[NowPlaying] Artwork loaded successfully, size: \(image.size)")
self.artworkImage = image
DispatchQueue.main.async {
self.updateNowPlayingInfo()
}
} else {
print("[NowPlaying] Failed to create image from data")
// Update Now Playing info without artwork on failure
DispatchQueue.main.async {
self.updateNowPlayingInfo()
}
}
}
artworkDownloadTask?.resume()
} else {
// No artwork URI provided - update immediately
print("[NowPlaying] No artwork URI provided")
artworkImage = nil
DispatchQueue.main.async {
self.updateNowPlayingInfo()
}
}
}
@objc func stop(completion: (() -> Void)? = nil) { @objc func stop(completion: (() -> Void)? = nil) {
guard !isStopping else { guard !isStopping else {
completion?() completion?()
@@ -513,27 +294,6 @@ class VlcPlayerView: ExpoView {
// Stop the media player // Stop the media player
mediaPlayer?.stop() mediaPlayer?.stop()
// Cancel any in-flight artwork downloads
artworkDownloadTask?.cancel()
artworkDownloadTask = nil
artworkImage = nil
// Cleanup remote command center targets
cleanupRemoteCommandCenter()
#if !os(tvOS)
// Deactivate audio session to allow other apps to use audio
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
print("Audio session deactivated")
} catch {
print("Failed to deactivate audio session: \(error)")
}
// Clear Now Playing info
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
#endif
// Remove observer // Remove observer
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
@@ -567,60 +327,6 @@ class VlcPlayerView: ExpoView {
"duration": durationMs, "duration": durationMs,
]) ])
} }
// Update Now Playing info to sync elapsed playback time
// iOS needs periodic updates to keep progress indicator in sync
DispatchQueue.main.async {
self.updateNowPlayingInfo()
}
}
private func updateNowPlayingInfo() {
#if !os(tvOS)
guard let player = self.mediaPlayer else { return }
var nowPlayingInfo = [String: Any]()
// Playback rate (0.0 = paused, 1.0 = playing at normal speed)
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.isPlaying ? player.rate : 0.0
// Current playback time in seconds
let currentTimeSeconds = Double(player.time.intValue) / 1000.0
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTimeSeconds
// Total duration in seconds
if let duration = player.media?.length.intValue {
let durationSeconds = Double(duration) / 1000.0
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = durationSeconds
}
// Add metadata if available
if let metadata = self.nowPlayingMetadata {
if let title = metadata["title"] {
nowPlayingInfo[MPMediaItemPropertyTitle] = title
print("[NowPlaying] Setting title: \(title)")
}
if let artist = metadata["artist"] {
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
print("[NowPlaying] Setting artist: \(artist)")
}
if let albumTitle = metadata["albumTitle"] {
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
print("[NowPlaying] Setting album: \(albumTitle)")
}
}
// Add artwork if available
if let artwork = self.artworkImage {
print("[NowPlaying] Setting artwork with size: \(artwork.size)")
let artworkItem = MPMediaItemArtwork(boundsSize: artwork.size) { _ in
return artwork
}
nowPlayingInfo[MPMediaItemPropertyArtwork] = artworkItem
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
#endif
} }
// MARK: - Expo Events // MARK: - Expo Events

3
nativewind-env.d.ts vendored
View File

@@ -1,2 +1,3 @@
/// <reference types="nativewind/types" /> /// <reference types="react-native-css/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by react-native-css. If you need to move or disable this file, please see the documentation.

View File

@@ -64,18 +64,19 @@
"i18next": "^25.0.0", "i18next": "^25.0.0",
"jotai": "^2.12.5", "jotai": "^2.12.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nativewind": "^2.0.11", "nativewind": "^5.0.0-preview.2",
"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.0.0", "react-i18next": "^15.4.0",
"react-native": "npm:react-native-tvos@0.81.5-1", "react-native": "npm:react-native-tvos@0.81.5-1",
"react-native-awesome-slider": "^2.9.0", "react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "^1.0.2", "react-native-bottom-tabs": "^1.0.2",
"react-native-circular-progress": "^1.4.1", "react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2", "react-native-collapsible": "^1.6.2",
"react-native-country-flag": "^2.0.2", "react-native-country-flag": "^2.0.2",
"react-native-device-info": "^15.0.0", "react-native-css": "^3.0.1",
"react-native-device-info": "^14.0.4",
"react-native-edge-to-edge": "^1.7.0", "react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-google-cast": "^4.9.1", "react-native-google-cast": "^4.9.1",
@@ -98,25 +99,28 @@
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1", "react-native-worklets": "0.5.1",
"sonner-native": "^0.21.0", "sonner-native": "^0.21.0",
"tailwindcss": "3.3.2", "tailwindcss": "^4.1.17",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"zod": "^4.1.3" "zod": "^4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.5", "@babel/core": "^7.20.0",
"@biomejs/biome": "2.3.5", "@biomejs/biome": "^2.3.5",
"@react-native-community/cli": "20.0.2", "@react-native-community/cli": "^20.0.0",
"@react-native-tvos/config-tv": "0.1.4", "@react-native-tvos/config-tv": "^0.1.1",
"@types/jest": "29.5.14", "@tailwindcss/postcss": "^4.1.17",
"@types/lodash": "4.17.20", "@types/jest": "^29.5.12",
"@types/lodash": "^4.17.15",
"@types/react": "~19.1.10", "@types/react": "~19.1.10",
"@types/react-test-renderer": "19.1.0", "@types/react-test-renderer": "^19.0.0",
"cross-env": "10.1.0", "cross-env": "^10.0.0",
"expo-doctor": "1.17.11", "expo-doctor": "^1.17.0",
"husky": "9.1.7", "husky": "^9.1.7",
"lint-staged": "16.2.6", "lint-staged": "^16.1.5",
"postcss": "^8.5.6",
"postinstall-postinstall": "^2.1.0",
"react-test-renderer": "19.1.1", "react-test-renderer": "19.1.1",
"typescript": "5.9.3" "typescript": "~5.9.2"
}, },
"expo": { "expo": {
"doctor": { "doctor": {
@@ -150,5 +154,8 @@
"resolutions": { "resolutions": {
"expo-constants": "~18.0.10", "expo-constants": "~18.0.10",
"expo-task-manager": "~14.0.8" "expo-task-manager": "~14.0.8"
},
"overrides": {
"lightningcss": "1.30.1"
} }
} }

View File

@@ -1,76 +1 @@
import { Platform } from "react-native"; export * from "expo-screen-orientation";
// Dummy exports for TV
enum DummyOrientationLock {
DEFAULT = 0,
ALL = 1,
PORTRAIT = 2,
PORTRAIT_UP = 3,
PORTRAIT_DOWN = 4,
LANDSCAPE = 5,
LANDSCAPE_LEFT = 6,
LANDSCAPE_RIGHT = 7,
OTHER = 8,
UNKNOWN = 9,
}
enum DummyOrientation {
UNKNOWN = 0,
PORTRAIT_UP = 1,
PORTRAIT_DOWN = 2,
LANDSCAPE_LEFT = 3,
LANDSCAPE_RIGHT = 4,
}
const dummyLockAsync = async () => {};
const dummyUnlockAsync = async () => {};
const dummyGetOrientationAsync = async () => DummyOrientation.UNKNOWN;
const dummyGetOrientationLockAsync = async () => DummyOrientationLock.DEFAULT;
const dummySupportsOrientationLockAsync = async () => false;
// Conditionally export based on platform
let ScreenOrientation: any;
if (!Platform.isTV) {
ScreenOrientation = require("expo-screen-orientation");
}
export const OrientationLock = Platform.isTV
? DummyOrientationLock
: ScreenOrientation?.OrientationLock;
export const Orientation = Platform.isTV
? DummyOrientation
: ScreenOrientation?.Orientation;
// Export types
export type OrientationLockType = typeof OrientationLock;
export type OrientationType = typeof Orientation;
export const lockAsync = Platform.isTV
? dummyLockAsync
: ScreenOrientation?.lockAsync;
export const unlockAsync = Platform.isTV
? dummyUnlockAsync
: ScreenOrientation?.unlockAsync;
export const getOrientationAsync = Platform.isTV
? dummyGetOrientationAsync
: ScreenOrientation?.getOrientationAsync;
export const getOrientationLockAsync = Platform.isTV
? dummyGetOrientationLockAsync
: ScreenOrientation?.getOrientationLockAsync;
export const supportsOrientationLockAsync = Platform.isTV
? dummySupportsOrientationLockAsync
: ScreenOrientation?.supportsOrientationLockAsync;
export const lockPlatformAsync = Platform.isTV
? dummyLockAsync
: ScreenOrientation?.lockPlatformAsync;
export const getPlatformLockAsync = Platform.isTV
? dummyGetOrientationLockAsync
: ScreenOrientation?.getPlatformLockAsync;
export const addOrientationChangeListener = Platform.isTV
? () => ({ remove: () => {} })
: ScreenOrientation?.addOrientationChangeListener;
export const removeOrientationChangeListener = Platform.isTV
? () => {}
: ScreenOrientation?.removeOrientationChangeListener;
export const removeOrientationChangeListeners = Platform.isTV
? () => {}
: ScreenOrientation?.removeOrientationChangeListeners;

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

View File

@@ -58,42 +58,31 @@ function useDownloadProvider() {
| Partial<JobStatus> | Partial<JobStatus>
| ((current: JobStatus) => Partial<JobStatus>), | ((current: JobStatus) => Partial<JobStatus>),
) => { ) => {
setProcesses((prev) => { setProcesses((prev) =>
const processIndex = prev.findIndex((p) => p.id === processId); prev.map((p) => {
if (processIndex === -1) return prev; if (p.id !== processId) return p;
const newStatus =
const currentProcess = prev[processIndex]; typeof updater === "function" ? updater(p) : updater;
if (!currentProcess) return prev; return {
...p,
const newStatus = ...newStatus,
typeof updater === "function" ? updater(currentProcess) : updater; };
}),
// Create new array with updated process );
const newProcesses = [...prev];
newProcesses[processIndex] = {
...currentProcess,
...newStatus,
};
return newProcesses;
});
}, },
[setProcesses], [setProcesses],
); );
const removeProcess = useCallback( const removeProcess = useCallback(
(id: string) => { (id: string) => {
// Use setTimeout to defer removal and avoid race conditions during rendering setProcesses((prev) => prev.filter((process) => process.id !== id));
setTimeout(() => {
setProcesses((prev) => prev.filter((process) => process.id !== id));
// Find and remove from task map // Find and remove from task map
taskMapRef.current.forEach((processId, taskId) => { taskMapRef.current.forEach((processId, taskId) => {
if (processId === id) { if (processId === id) {
taskMapRef.current.delete(taskId); taskMapRef.current.delete(taskId);
} }
}); });
}, 0);
}, },
[setProcesses], [setProcesses],
); );

View File

@@ -6,20 +6,16 @@ import type {
import { Directory, File, Paths } from "expo-file-system"; import { Directory, File, Paths } from "expo-file-system";
import { getItemImage } from "@/utils/getItemImage"; import { getItemImage } from "@/utils/getItemImage";
import { fetchAndParseSegments } from "@/utils/segments"; import { fetchAndParseSegments } from "@/utils/segments";
import { filePathToUri } from "@/utils/storage";
import { generateTrickplayUrl, getTrickplayInfo } from "@/utils/trickplay"; import { generateTrickplayUrl, getTrickplayInfo } from "@/utils/trickplay";
import type { MediaTimeSegment, TrickPlayData } from "./types"; import type { MediaTimeSegment, TrickPlayData } from "./types";
import { generateFilename } from "./utils"; import { generateFilename } from "./utils";
/** /**
* Downloads trickplay images for an item * Downloads trickplay images for an item
* @param item - The item to download trickplay images for
* @param storagePath - Optional custom storage path (for Android SD card support)
* @returns TrickPlayData with path and size, or undefined if not available * @returns TrickPlayData with path and size, or undefined if not available
*/ */
export async function downloadTrickplayImages( export async function downloadTrickplayImages(
item: BaseItemDto, item: BaseItemDto,
storagePath?: string,
): Promise<TrickPlayData | undefined> { ): Promise<TrickPlayData | undefined> {
const trickplayInfo = getTrickplayInfo(item); const trickplayInfo = getTrickplayInfo(item);
if (!trickplayInfo || !item.Id) { if (!trickplayInfo || !item.Id) {
@@ -27,11 +23,7 @@ export async function downloadTrickplayImages(
} }
const filename = generateFilename(item); const filename = generateFilename(item);
const trickplayDir = new Directory(Paths.document, `${filename}_trickplay`);
// Use custom storage path if provided (Android SD card), otherwise use Paths.document
const trickplayDir = storagePath
? new Directory(filePathToUri(storagePath), `${filename}_trickplay`)
: new Directory(Paths.document, `${filename}_trickplay`);
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!trickplayDir.exists) { if (!trickplayDir.exists) {
@@ -77,17 +69,12 @@ export async function downloadTrickplayImages(
/** /**
* Downloads external subtitle files and updates their delivery URLs to local paths * Downloads external subtitle files and updates their delivery URLs to local paths
* @param mediaSource - The media source containing subtitle information
* @param item - The item to download subtitles for
* @param apiBasePath - The base path for the API
* @param storagePath - Optional custom storage path (for Android SD card support)
* @returns Updated media source with local subtitle paths * @returns Updated media source with local subtitle paths
*/ */
export async function downloadSubtitles( export async function downloadSubtitles(
mediaSource: MediaSourceInfo, mediaSource: MediaSourceInfo,
item: BaseItemDto, item: BaseItemDto,
apiBasePath: string, apiBasePath: string,
storagePath?: string,
): Promise<MediaSourceInfo> { ): Promise<MediaSourceInfo> {
const externalSubtitles = mediaSource.MediaStreams?.filter( const externalSubtitles = mediaSource.MediaStreams?.filter(
(stream) => (stream) =>
@@ -104,17 +91,10 @@ export async function downloadSubtitles(
const url = apiBasePath + subtitle.DeliveryUrl; const url = apiBasePath + subtitle.DeliveryUrl;
const extension = subtitle.Codec || "srt"; const extension = subtitle.Codec || "srt";
const destination = new File(
// Use custom storage path if provided (Android SD card), otherwise use Paths.document Paths.document,
const destination = storagePath `${filename}_subtitle_${subtitle.Index}.${extension}`,
? new File( );
filePathToUri(storagePath),
`${filename}_subtitle_${subtitle.Index}.${extension}`,
)
: new File(
Paths.document,
`${filename}_subtitle_${subtitle.Index}.${extension}`,
);
// Skip if already exists // Skip if already exists
if (destination.exists) { if (destination.exists) {
@@ -228,21 +208,13 @@ export async function downloadAdditionalAssets(params: {
api: Api; api: Api;
saveImageFn: (itemId: string, url?: string) => Promise<void>; saveImageFn: (itemId: string, url?: string) => Promise<void>;
saveSeriesImageFn: (item: BaseItemDto) => Promise<void>; saveSeriesImageFn: (item: BaseItemDto) => Promise<void>;
storagePath?: string;
}): Promise<{ }): Promise<{
trickPlayData?: TrickPlayData; trickPlayData?: TrickPlayData;
updatedMediaSource: MediaSourceInfo; updatedMediaSource: MediaSourceInfo;
introSegments?: MediaTimeSegment[]; introSegments?: MediaTimeSegment[];
creditSegments?: MediaTimeSegment[]; creditSegments?: MediaTimeSegment[];
}> { }> {
const { const { item, mediaSource, api, saveImageFn, saveSeriesImageFn } = params;
item,
mediaSource,
api,
saveImageFn,
saveSeriesImageFn,
storagePath,
} = params;
// Run all downloads in parallel for speed // Run all downloads in parallel for speed
const [ const [
@@ -251,11 +223,11 @@ export async function downloadAdditionalAssets(params: {
segments, segments,
// Cover images (fire and forget, errors are logged) // Cover images (fire and forget, errors are logged)
] = await Promise.all([ ] = await Promise.all([
downloadTrickplayImages(item, storagePath), downloadTrickplayImages(item),
// Only download subtitles for non-transcoded streams // Only download subtitles for non-transcoded streams
mediaSource.TranscodingUrl mediaSource.TranscodingUrl
? Promise.resolve(mediaSource) ? Promise.resolve(mediaSource)
: downloadSubtitles(mediaSource, item, api.basePath || "", storagePath), : downloadSubtitles(mediaSource, item, api.basePath || ""),
item.Id item.Id
? fetchSegments(item.Id, api) ? fetchSegments(item.Id, api)
: Promise.resolve({ : Promise.resolve({

View File

@@ -1,4 +1,4 @@
import { Directory, File } from "expo-file-system"; import { Directory, File, Paths } from "expo-file-system";
import { getAllDownloadedItems, getDownloadedItemById } from "./database"; import { getAllDownloadedItems, getDownloadedItemById } from "./database";
import type { DownloadedItem } from "./types"; import type { DownloadedItem } from "./types";
import { filePathToUri } from "./utils"; import { filePathToUri } from "./utils";
@@ -39,11 +39,13 @@ export function deleteAllAssociatedFiles(item: DownloadedItem): void {
stream.DeliveryUrl stream.DeliveryUrl
) { ) {
try { try {
// Use the full path from DeliveryUrl (it's already a full file:// URI) const subtitleFilename = stream.DeliveryUrl.split("/").pop();
const subtitleFile = new File(stream.DeliveryUrl); if (subtitleFilename) {
if (subtitleFile.exists) { const subtitleFile = new File(Paths.document, subtitleFilename);
subtitleFile.delete(); if (subtitleFile.exists) {
console.log(`[DELETE] Subtitle deleted: ${stream.DeliveryUrl}`); subtitleFile.delete();
console.log(`[DELETE] Subtitle deleted: ${subtitleFilename}`);
}
} }
} catch (error) { } catch (error) {
console.error("[DELETE] Failed to delete subtitle:", error); console.error("[DELETE] Failed to delete subtitle:", error);
@@ -55,13 +57,15 @@ export function deleteAllAssociatedFiles(item: DownloadedItem): void {
// Delete trickplay directory // Delete trickplay directory
if (item.trickPlayData?.path) { if (item.trickPlayData?.path) {
try { try {
// Use the full path from trickPlayData (it's already a full file:// URI) const trickplayDirName = item.trickPlayData.path.split("/").pop();
const trickplayDir = new Directory(item.trickPlayData.path); if (trickplayDirName) {
if (trickplayDir.exists) { const trickplayDir = new Directory(Paths.document, trickplayDirName);
trickplayDir.delete(); if (trickplayDir.exists) {
console.log( trickplayDir.delete();
`[DELETE] Trickplay directory deleted: ${item.trickPlayData.path}`, console.log(
); `[DELETE] Trickplay directory deleted: ${trickplayDirName}`,
);
}
} }
} catch (error) { } catch (error) {
console.error("[DELETE] Failed to delete trickplay directory:", error); console.error("[DELETE] Failed to delete trickplay directory:", error);

View File

@@ -6,16 +6,13 @@ import { File, Paths } from "expo-file-system";
import type { MutableRefObject } from "react"; import type { MutableRefObject } from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import DeviceInfo from "react-native-device-info"; import DeviceInfo from "react-native-device-info";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import type { Bitrate } from "@/components/BitrateSelector"; import type { Bitrate } from "@/components/BitrateSelector";
import useImageStorage from "@/hooks/useImageStorage"; import useImageStorage from "@/hooks/useImageStorage";
import { BackgroundDownloader } from "@/modules"; import { BackgroundDownloader } from "@/modules";
import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device"; import { getOrSetDeviceId } from "@/utils/device";
import useDownloadHelper from "@/utils/download"; import useDownloadHelper from "@/utils/download";
import { getStoragePath } from "@/utils/storage";
import { downloadAdditionalAssets } from "../additionalDownloads"; import { downloadAdditionalAssets } from "../additionalDownloads";
import { import {
clearAllDownloadedItems, clearAllDownloadedItems,
@@ -52,7 +49,6 @@ export function useDownloadOperations({
onDataChange, onDataChange,
}: UseDownloadOperationsProps) { }: UseDownloadOperationsProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { settings } = useSettings();
const { saveSeriesPrimaryImage } = useDownloadHelper(); const { saveSeriesPrimaryImage } = useDownloadHelper();
const { saveImage } = useImageStorage(); const { saveImage } = useImageStorage();
@@ -83,12 +79,6 @@ export function useDownloadOperations({
return; return;
} }
// Get storage path if custom location is set
let storagePath: string | undefined;
if (Platform.OS === "android" && settings.downloadStorageLocation) {
storagePath = await getStoragePath(settings.downloadStorageLocation);
}
// Download all additional assets BEFORE starting native video download // Download all additional assets BEFORE starting native video download
const additionalAssets = await downloadAdditionalAssets({ const additionalAssets = await downloadAdditionalAssets({
item, item,
@@ -96,7 +86,6 @@ export function useDownloadOperations({
api, api,
saveImageFn: saveImage, saveImageFn: saveImage,
saveSeriesImageFn: saveSeriesPrimaryImage, saveSeriesImageFn: saveSeriesPrimaryImage,
storagePath,
}); });
// Ensure URL is absolute (not relative) before storing // Ensure URL is absolute (not relative) before storing
@@ -130,19 +119,10 @@ export function useDownloadOperations({
// Add to processes // Add to processes
setProcesses((prev) => [...prev, jobStatus]); setProcesses((prev) => [...prev, jobStatus]);
// Generate destination path using custom storage location if set // Generate destination path
const filename = generateFilename(item); const filename = generateFilename(item);
let destinationPath: string; const videoFile = new File(Paths.document, `${filename}.mp4`);
const destinationPath = uriToFilePath(videoFile.uri);
if (storagePath) {
// Use custom storage location
destinationPath = `${storagePath}/${filename}.mp4`;
console.log(`[DOWNLOAD] Using custom storage: ${destinationPath}`);
} else {
// Use default Paths.document
const videoFile = new File(Paths.document, `${filename}.mp4`);
destinationPath = uriToFilePath(videoFile.uri);
}
console.log(`[DOWNLOAD] Starting video: ${item.Name}`); console.log(`[DOWNLOAD] Starting video: ${item.Name}`);
console.log(`[DOWNLOAD] Download URL: ${downloadUrl}`); console.log(`[DOWNLOAD] Download URL: ${downloadUrl}`);

View File

@@ -1,13 +1,8 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type * as NotificationsType from "expo-notifications"; import * as Notifications from "expo-notifications";
import type { TFunction } from "i18next"; import type { TFunction } from "i18next";
import { Platform } from "react-native"; import { Platform } from "react-native";
// Conditionally import expo-notifications only on non-TV platforms
const Notifications = Platform.isTV
? null
: (require("expo-notifications") as typeof NotificationsType);
/** /**
* Generate notification content based on item type * Generate notification content based on item type
*/ */
@@ -65,7 +60,7 @@ export async function sendDownloadNotification(
body: string, body: string,
data?: Record<string, any>, data?: Record<string, any>,
): Promise<void> { ): Promise<void> {
if (Platform.isTV || !Notifications) return; if (Platform.isTV) return;
try { try {
await Notifications.scheduleNotificationAsync({ await Notifications.scheduleNotificationAsync({

View File

@@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin( setJellyfin(
() => () =>
new Jellyfin({ new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.48.0" }, clientInfo: { name: "Streamyfin", version: "0.47.1" },
deviceInfo: { deviceInfo: {
name: deviceName, name: deviceName,
id, id,
@@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return { return {
authorization: `MediaBrowser Client="Streamyfin", Device=${ authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS" Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.48.0"`, }, DeviceId="${deviceId}", Version="0.47.1"`,
}; };
}, [deviceId]); }, [deviceId]);

View File

@@ -3,51 +3,18 @@
const isTV = process.env?.EXPO_TV === "1"; const isTV = process.env?.EXPO_TV === "1";
const disableForTV = (_moduleName) =>
isTV
? {
platforms: {
ios: null,
android: null,
},
}
: undefined;
const dependencies = {
"react-native-volume-manager": !isTV
? {
platforms: {
// leaving this blank seems to enable auto-linking which is what we want for mobile
},
}
: {
platforms: {
android: null,
},
},
"expo-notifications": disableForTV("expo-notifications"),
"react-native-image-colors": disableForTV("react-native-image-colors"),
"expo-sharing": disableForTV("expo-sharing"),
"expo-haptics": disableForTV("expo-haptics"),
"expo-brightness": disableForTV("expo-brightness"),
"expo-sensors": disableForTV("expo-sensors"),
"expo-screen-orientation": disableForTV("expo-screen-orientation"),
"react-native-ios-context-menu": disableForTV(
"react-native-ios-context-menu",
),
"react-native-ios-utilities": disableForTV("react-native-ios-utilities"),
"react-native-pager-view": disableForTV("react-native-pager-view"),
};
// Filter out undefined values
const cleanDependencies = Object.fromEntries(
Object.entries(dependencies).filter(([_, value]) => value !== undefined),
);
module.exports = { module.exports = {
dependencies: cleanDependencies, dependencies: {
project: { "react-native-volume-manager": !isTV
ios: {}, ? {
android: {}, platforms: {
// leaving this blank seems to enable auto-linking which is what we want for mobile
},
}
: {
platforms: {
android: null,
},
},
}, },
}; };

View File

@@ -1,10 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -337,8 +337,7 @@
"audio": "Audio", "audio": "Audio",
"subtitle": "Subtitle", "subtitle": "Subtitle",
"play": "Play", "play": "Play",
"none": "None", "none": "None"
"track": "Track"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -445,7 +444,6 @@
"no_similar_items_found": "No Similar Items Found", "no_similar_items_found": "No Similar Items Found",
"video": "Video", "video": "Video",
"more_details": "More Details", "more_details": "More Details",
"media_options": "Media Options",
"quality": "Quality", "quality": "Quality",
"audio": "Audio", "audio": "Audio",
"subtitles": "Subtitle", "subtitles": "Subtitle",

View File

@@ -33,7 +33,8 @@
"*.ts", "*.ts",
"*.tsx", "*.tsx",
".expo/types/**/*.ts", ".expo/types/**/*.ts",
"expo-env.d.ts" "expo-env.d.ts",
"nativewind-env.d.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",

View File

@@ -26,7 +26,7 @@ export type DownloadOption = {
}; };
export const ScreenOrientationEnum: Record< export const ScreenOrientationEnum: Record<
(typeof ScreenOrientation.OrientationLock)[keyof typeof ScreenOrientation.OrientationLock], ScreenOrientation.OrientationLock,
string string
> = { > = {
[ScreenOrientation.OrientationLock.DEFAULT]: [ScreenOrientation.OrientationLock.DEFAULT]:
@@ -145,7 +145,6 @@ export type Settings = {
marlinServerUrl?: string; marlinServerUrl?: string;
openInVLC?: boolean; openInVLC?: boolean;
downloadQuality?: DownloadOption; downloadQuality?: DownloadOption;
downloadStorageLocation?: string;
defaultBitrate?: Bitrate; defaultBitrate?: Bitrate;
libraryOptions: LibraryOptions; libraryOptions: LibraryOptions;
defaultAudioLanguage: CultureDto | null; defaultAudioLanguage: CultureDto | null;
@@ -155,7 +154,7 @@ export type Settings = {
subtitleMode: SubtitlePlaybackMode; subtitleMode: SubtitlePlaybackMode;
rememberSubtitleSelections: boolean; rememberSubtitleSelections: boolean;
showHomeTitles: boolean; showHomeTitles: boolean;
defaultVideoOrientation: (typeof ScreenOrientation.OrientationLock)[keyof typeof ScreenOrientation.OrientationLock]; defaultVideoOrientation: ScreenOrientation.OrientationLock;
forwardSkipTime: number; forwardSkipTime: number;
rewindSkipTime: number; rewindSkipTime: number;
showCustomMenuLinks: boolean; showCustomMenuLinks: boolean;
@@ -204,7 +203,6 @@ export const defaultValues: Settings = {
marlinServerUrl: "", marlinServerUrl: "",
openInVLC: false, openInVLC: false,
downloadQuality: DownloadOptions[0], downloadQuality: DownloadOptions[0],
downloadStorageLocation: undefined,
defaultBitrate: BITRATES[0], defaultBitrate: BITRATES[0],
libraryOptions: { libraryOptions: {
display: "list", display: "list",

View File

@@ -1,143 +0,0 @@
import { Directory, Paths } from "expo-file-system";
import { Platform } from "react-native";
import { BackgroundDownloader, type StorageLocation } from "@/modules";
let cachedStorageLocations: StorageLocation[] | null = null;
// Debug mode: Set to true to simulate an SD card for testing in emulator
// This creates a real writable directory that mimics SD card behavior
const DEBUG_SIMULATE_SD_CARD = false;
/**
* Get all available storage locations (Android only)
* Returns cached result on subsequent calls
*/
export async function getAvailableStorageLocations(): Promise<
StorageLocation[]
> {
if (Platform.OS !== "android") {
return [];
}
if (cachedStorageLocations !== null) {
return cachedStorageLocations;
}
try {
const locations = await BackgroundDownloader.getAvailableStorageLocations();
// Debug mode: Add a functional simulated SD card for testing
if (DEBUG_SIMULATE_SD_CARD && locations.length === 1) {
// Use a real writable path within the app's document directory
const sdcardSimDir = new Directory(Paths.document, "sdcard_sim");
// Create the directory if it doesn't exist
if (!sdcardSimDir.exists) {
sdcardSimDir.create({ intermediates: true });
}
const mockSdCard: StorageLocation = {
id: "sdcard_sim",
path: sdcardSimDir.uri.replace("file://", ""),
type: "external",
label: "SD Card (Simulated)",
totalSpace: 64 * 1024 * 1024 * 1024, // 64 GB
freeSpace: 32 * 1024 * 1024 * 1024, // 32 GB free
};
locations.push(mockSdCard);
console.log("[DEBUG] Added simulated SD card:", mockSdCard.path);
}
cachedStorageLocations = locations;
return locations;
} catch (error) {
console.error("Failed to get storage locations:", error);
return [];
}
}
/**
* Clear the cached storage locations
* Useful when storage configuration might have changed
*/
export function clearStorageLocationsCache(): void {
cachedStorageLocations = null;
console.log("[Storage] Cache cleared");
}
/**
* Get a simplified label for a storage location ID
* @param storageId - The storage location ID (e.g., "internal", "sdcard_0")
* @returns Human-readable label (e.g., "Internal Storage", "SD Card")
*/
export async function getStorageLabel(storageId?: string): Promise<string> {
if (!storageId || Platform.OS !== "android") {
return "Internal Storage";
}
const locations = await getAvailableStorageLocations();
const location = locations.find((loc) => loc.id === storageId);
return location?.label || "Internal Storage";
}
/**
* Get the filesystem path for a storage location ID
* @param storageId - The storage location ID (e.g., "internal", "sdcard_0")
* @returns The filesystem path, or default path if not found
*/
export async function getStoragePath(storageId?: string): Promise<string> {
if (!storageId || Platform.OS !== "android") {
return getDefaultStoragePath();
}
const locations = await getAvailableStorageLocations();
const location = locations.find((loc) => loc.id === storageId);
if (!location) {
console.warn(`Storage location not found: ${storageId}, using default`);
return getDefaultStoragePath();
}
return location.path;
}
/**
* Get the default storage path (current behavior using Paths.document)
* @returns The default storage path
*/
export function getDefaultStoragePath(): string {
// Paths.document returns a Directory with a URI like "file:///data/user/0/..."
// We need to extract the actual path
const uri = Paths.document.uri;
return uri.replace("file://", "");
}
/**
* Get a storage location by ID
* @param storageId - The storage location ID
* @returns The storage location or undefined if not found
*/
export async function getStorageLocationById(
storageId?: string,
): Promise<StorageLocation | undefined> {
if (!storageId || Platform.OS !== "android") {
return undefined;
}
const locations = await getAvailableStorageLocations();
return locations.find((loc) => loc.id === storageId);
}
/**
* Convert plain file path to file:// URI
* Required for expo-file-system File constructor
* @param path - The file path
* @returns The file:// URI
*/
export function filePathToUri(path: string): string {
if (path.startsWith("file://")) {
return path;
}
return `file://${path}`;
}