Compare commits
4 Commits
feat/setti
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dbe5bb64c | ||
|
|
801ab275ab | ||
|
|
f7033e7abb | ||
|
|
0d796d01b8 |
38
.github/workflows/detect-duplicate.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: 🔁 Detect Duplicate Issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: detect-duplicate-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
name: 🔍 Find similar issues
|
||||||
|
if: github.actor != 'github-actions[bot]'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: 🍞 Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: 🔍 Detect duplicate issues
|
||||||
|
run: bun scripts/detect-duplicate-issue.mjs
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||||
|
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||||
@@ -243,28 +243,6 @@ export default function IndexLayout() {
|
|||||||
headerLeft: () => <HeaderBackButton />,
|
headerLeft: () => <HeaderBackButton />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name='settings/account/page'
|
|
||||||
options={{
|
|
||||||
title: t("home.settings.account.title"),
|
|
||||||
headerShown: !Platform.isTV,
|
|
||||||
headerBlurEffect: "none",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
headerShadowVisible: false,
|
|
||||||
headerLeft: () => <HeaderBackButton />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='settings/notifications/page'
|
|
||||||
options={{
|
|
||||||
title: t("home.settings.notifications.title"),
|
|
||||||
headerShown: !Platform.isTV,
|
|
||||||
headerBlurEffect: "none",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
headerShadowVisible: false,
|
|
||||||
headerLeft: () => <HeaderBackButton />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||||
<Stack.Screen key={name} name={name} options={options} />
|
<Stack.Screen key={name} name={name} options={options} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,44 +1,38 @@
|
|||||||
import { useNavigation } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useAtom } from "jotai";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
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";
|
||||||
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
||||||
import { SettingsHero } from "@/components/settings/index/SettingsHero";
|
import { QuickConnect } from "@/components/settings/QuickConnect";
|
||||||
import { SettingsRow } from "@/components/settings/index/SettingsRow";
|
|
||||||
import { SettingsSearchBar } from "@/components/settings/index/SettingsSearchBar";
|
|
||||||
import { SettingsSection } from "@/components/settings/index/SettingsSection";
|
|
||||||
import {
|
|
||||||
SETTINGS_CATALOG,
|
|
||||||
type SettingsTarget,
|
|
||||||
} from "@/components/settings/index/settingsCatalog";
|
|
||||||
import { useSettingsSearch } from "@/components/settings/index/useSettingsSearch";
|
|
||||||
import {
|
|
||||||
QuickConnectSheet,
|
|
||||||
type QuickConnectSheetRef,
|
|
||||||
} from "@/components/settings/QuickConnect";
|
|
||||||
import { StorageSettings } from "@/components/settings/StorageSettings";
|
import { StorageSettings } from "@/components/settings/StorageSettings";
|
||||||
|
import { UserInfo } from "@/components/settings/UserInfo";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|
||||||
|
// TV-specific settings component
|
||||||
const SettingsTV = Platform.isTV ? require("./settings.tv").default : null;
|
const SettingsTV = Platform.isTV ? require("./settings.tv").default : null;
|
||||||
|
|
||||||
|
// Mobile settings component
|
||||||
function SettingsMobile() {
|
function SettingsMobile() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const [_user] = useAtom(userAtom);
|
||||||
const { logout } = useJellyfin();
|
const { logout } = useJellyfin();
|
||||||
const navigation = useNavigation();
|
|
||||||
const quickConnectRef = useRef<QuickConnectSheetRef>(null);
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const os: "ios" | "android" = Platform.OS === "ios" ? "ios" : "android";
|
|
||||||
const results = useSettingsSearch(query);
|
|
||||||
const searching = query.trim().length > 0;
|
|
||||||
|
|
||||||
|
const navigation = useNavigation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity onPress={() => logout()}>
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
logout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text className='text-red-600 px-2'>
|
<Text className='text-red-600 px-2'>
|
||||||
{t("home.settings.log_out_button")}
|
{t("home.settings.log_out_button")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -47,95 +41,98 @@ function SettingsMobile() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTarget = (target: SettingsTarget) => {
|
|
||||||
if (target.type === "action") {
|
|
||||||
if (target.action === "quickConnect") {
|
|
||||||
quickConnectRef.current?.present();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.push(target.route as any);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentInsetAdjustmentBehavior='automatic'
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
keyboardShouldPersistTaps='handled'
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingLeft: insets.left,
|
paddingLeft: insets.left,
|
||||||
paddingRight: insets.right,
|
paddingRight: insets.right,
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 32,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!searching && (
|
<View
|
||||||
<SettingsHero
|
className='p-4 flex flex-col'
|
||||||
onPress={() => router.push("/settings/account/page" as any)}
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
/>
|
>
|
||||||
)}
|
<View className='mb-4'>
|
||||||
<SettingsSearchBar value={query} onChange={setQuery} />
|
<UserInfo />
|
||||||
|
</View>
|
||||||
|
|
||||||
{searching ? (
|
<QuickConnect className='mb-4' />
|
||||||
<SettingsSection title={t("home.settings.search_results")}>
|
|
||||||
{results.length === 0 ? (
|
{Platform.OS !== "ios" && (
|
||||||
<View className='px-4 py-3'>
|
<View className='mb-4'>
|
||||||
<Text className='text-[#9899A1]'>
|
<ListGroup title={t("pairing.pair_with_phone_title")}>
|
||||||
{t("home.settings.search_no_results")}
|
<ListItem
|
||||||
</Text>
|
onPress={() =>
|
||||||
</View>
|
router.push("/(auth)/(tabs)/(home)/companion-login")
|
||||||
) : (
|
}
|
||||||
results.map((r, i) => (
|
title={t("pairing.pair_with_phone")}
|
||||||
<SettingsRow
|
textColor='blue'
|
||||||
key={r.id}
|
|
||||||
title={r.title}
|
|
||||||
icon={r.icon}
|
|
||||||
value={r.subtitle}
|
|
||||||
onPress={() => handleTarget(r.target)}
|
|
||||||
isLast={i === results.length - 1}
|
|
||||||
/>
|
/>
|
||||||
))
|
</ListGroup>
|
||||||
)}
|
|
||||||
</SettingsSection>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<View className='mx-3 mb-5'>
|
|
||||||
<AppLanguageSelector />
|
|
||||||
</View>
|
</View>
|
||||||
{SETTINGS_CATALOG.map((section) => {
|
)}
|
||||||
const entries = section.entries.filter(
|
|
||||||
(e) => !e.platforms || e.platforms.includes(os),
|
|
||||||
);
|
|
||||||
if (entries.length === 0) return null;
|
|
||||||
return (
|
|
||||||
<SettingsSection key={section.id} title={t(section.titleKey)}>
|
|
||||||
{entries.map((e, i) => (
|
|
||||||
<SettingsRow
|
|
||||||
key={e.id}
|
|
||||||
title={t(e.titleKey)}
|
|
||||||
icon={e.icon}
|
|
||||||
onPress={() => handleTarget(e.target)}
|
|
||||||
isLast={i === entries.length - 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SettingsSection>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<SettingsSection>
|
|
||||||
<View className='p-3'>
|
|
||||||
<StorageSettings />
|
|
||||||
</View>
|
|
||||||
</SettingsSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<QuickConnectSheet ref={quickConnectRef} />
|
<View className='mb-4'>
|
||||||
|
<AppLanguageSelector />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='mb-4'>
|
||||||
|
<ListGroup title={t("home.settings.categories.title")}>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/playback-controls/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.playback_controls.title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/audio-subtitles/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.audio_subtitles.title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/music/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.music.title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/appearance/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.appearance.title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/plugins/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.plugins.plugins_title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/intro/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.intro.title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/network/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.network.title")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/logs/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.logs.logs_title")}
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<StorageSettings />
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function settings() {
|
export default function settings() {
|
||||||
|
// Use TV settings component on TV platforms
|
||||||
if (Platform.isTV && SettingsTV) {
|
if (Platform.isTV && SettingsTV) {
|
||||||
return <SettingsTV />;
|
return <SettingsTV />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SettingsMobile />;
|
return <SettingsMobile />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import * as Application from "expo-application";
|
|
||||||
import { setStringAsync } from "expo-clipboard";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Alert, ScrollView } from "react-native";
|
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
|
|
||||||
export default function AccountPage() {
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
const [revealed, setRevealed] = useState(false);
|
|
||||||
const success = useHaptic("success");
|
|
||||||
const version = Application.nativeApplicationVersion ?? "N/A";
|
|
||||||
const token = api?.accessToken ?? "";
|
|
||||||
const masked = token ? `•••• •••• •••• ${token.slice(-4)}` : "";
|
|
||||||
|
|
||||||
const copyToken = async () => {
|
|
||||||
if (!token) return;
|
|
||||||
try {
|
|
||||||
await setStringAsync(token);
|
|
||||||
success();
|
|
||||||
Alert.alert(t("home.settings.account.copied"));
|
|
||||||
} catch {
|
|
||||||
Alert.alert(t("home.settings.account.copy_failed"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView contentContainerStyle={{ padding: 16 }}>
|
|
||||||
<ListGroup title={t("home.settings.user_info.user_info_title")}>
|
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.user_info.user")}
|
|
||||||
value={user?.Name}
|
|
||||||
/>
|
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.user_info.server")}
|
|
||||||
value={api?.basePath}
|
|
||||||
/>
|
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.user_info.token")}
|
|
||||||
value={revealed ? token : masked}
|
|
||||||
onPress={() => setRevealed((r) => !r)}
|
|
||||||
/>
|
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.account.copy_token")}
|
|
||||||
textColor='blue'
|
|
||||||
onPress={copyToken}
|
|
||||||
/>
|
|
||||||
<ListItem
|
|
||||||
title={t("home.settings.user_info.app_version")}
|
|
||||||
value={version}
|
|
||||||
/>
|
|
||||||
</ListGroup>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import * as Notifications from "expo-notifications";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Linking, ScrollView, Switch, View } from "react-native";
|
|
||||||
import { Button } from "@/components/Button";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
|
||||||
const { settings, updateSettings } = useSettings();
|
|
||||||
const [perm, setPerm] =
|
|
||||||
useState<Notifications.NotificationPermissionsStatus | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Notifications.getPermissionsAsync().then(setPerm);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const requestPermission = async () => {
|
|
||||||
const res = await Notifications.requestPermissionsAsync();
|
|
||||||
setPerm(res);
|
|
||||||
// Only bounce to system settings when the OS will not prompt again.
|
|
||||||
if (!res.granted && res.canAskAgain === false) {
|
|
||||||
Linking.openSettings();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!settings || perm === null) return null;
|
|
||||||
|
|
||||||
if (!perm.granted) {
|
|
||||||
return (
|
|
||||||
<View className='flex-1 items-center justify-center px-8'>
|
|
||||||
<Ionicons name='notifications-off-outline' size={56} color='#5A5960' />
|
|
||||||
<Text className='text-white text-lg font-semibold mt-4 text-center'>
|
|
||||||
{t("home.settings.notifications.disabled_title")}
|
|
||||||
</Text>
|
|
||||||
<Text className='text-[#9899A1] text-center mt-2'>
|
|
||||||
{t("home.settings.notifications.disabled_description")}
|
|
||||||
</Text>
|
|
||||||
<Button color='purple' className='mt-6' onPress={requestPermission}>
|
|
||||||
{t("home.settings.notifications.enable_button")}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView contentContainerStyle={{ padding: 16 }}>
|
|
||||||
<ListGroup title={t("home.settings.notifications.events_title")}>
|
|
||||||
<ListItem title={t("home.settings.notifications.master")}>
|
|
||||||
<Switch
|
|
||||||
value={settings.notificationsEnabled}
|
|
||||||
onValueChange={(v) => updateSettings({ notificationsEnabled: v })}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem title={t("home.settings.notifications.downloads")}>
|
|
||||||
<Switch
|
|
||||||
value={settings.notifyDownloads}
|
|
||||||
disabled={!settings.notificationsEnabled}
|
|
||||||
onValueChange={(v) => updateSettings({ notifyDownloads: v })}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</ListGroup>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,16 +20,18 @@ export default function PlaybackControlsPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className='p-4'
|
className='p-4 flex flex-col'
|
||||||
style={{ gap: 16, paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
>
|
>
|
||||||
<MediaProvider>
|
<View className='mb-4'>
|
||||||
<MediaToggles />
|
<MediaProvider>
|
||||||
<GestureControls />
|
<MediaToggles className='mb-4' />
|
||||||
<PlaybackControlsSettings />
|
<GestureControls className='mb-4' />
|
||||||
<MpvBufferSettings />
|
<PlaybackControlsSettings />
|
||||||
<MpvVoSettings />
|
<MpvBufferSettings />
|
||||||
</MediaProvider>
|
<MpvVoSettings />
|
||||||
|
</MediaProvider>
|
||||||
|
</View>
|
||||||
{!Platform.isTV && <ChromecastSettings />}
|
{!Platform.isTV && <ChromecastSettings />}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ export default function TabLayout() {
|
|||||||
!settings?.streamyStatsServerUrl || settings?.hideWatchlistsTab,
|
!settings?.streamyStatsServerUrl || settings?.hideWatchlistsTab,
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS === "android"
|
Platform.OS === "android"
|
||||||
? (_e) => require("@/assets/icons/list.png")
|
? (_e) => require("@/assets/icons/list.star.png")
|
||||||
: (_e) => ({ sfSymbol: "list.bullet.rectangle" }),
|
: (_e) => ({ sfSymbol: "list.star" }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
@@ -112,7 +112,7 @@ export default function TabLayout() {
|
|||||||
title: t("tabs.library"),
|
title: t("tabs.library"),
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS === "android"
|
Platform.OS === "android"
|
||||||
? (_e) => require("@/assets/icons/server.rack.png")
|
? (_e) => require("@/assets/icons/rectangle.stack.fill.png")
|
||||||
: (_e) => ({ sfSymbol: "rectangle.stack.fill" }),
|
: (_e) => ({ sfSymbol: "rectangle.stack.fill" }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -123,8 +123,8 @@ export default function TabLayout() {
|
|||||||
tabBarItemHidden: !settings?.showCustomMenuLinks,
|
tabBarItemHidden: !settings?.showCustomMenuLinks,
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS === "android"
|
Platform.OS === "android"
|
||||||
? (_e) => require("@/assets/icons/list.png")
|
? (_e) => require("@/assets/icons/link.png")
|
||||||
: (_e) => ({ sfSymbol: "list.dash.fill" }),
|
: (_e) => ({ sfSymbol: "link" }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
@@ -134,7 +134,7 @@ export default function TabLayout() {
|
|||||||
tabBarItemHidden: !Platform.isTV,
|
tabBarItemHidden: !Platform.isTV,
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS === "android"
|
Platform.OS === "android"
|
||||||
? (_e) => require("@/assets/icons/gear.png") //Should maybe use other libraries to have it uniform
|
? (_e) => require("@/assets/icons/gearshape.fill.png")
|
||||||
: (_e) => ({ sfSymbol: "gearshape.fill" }),
|
: (_e) => ({ sfSymbol: "gearshape.fill" }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -439,21 +439,15 @@ export default function DirectPlayerPage() {
|
|||||||
if (!item?.Id || !stream?.sessionId || offline || !api) return;
|
if (!item?.Id || !stream?.sessionId || offline || !api) return;
|
||||||
|
|
||||||
const currentTimeInTicks = msToTicks(progress.get());
|
const currentTimeInTicks = msToTicks(progress.get());
|
||||||
await getPlaystateApi(api).onPlaybackStopped({
|
await getPlaystateApi(api).reportPlaybackStopped({
|
||||||
itemId: item.Id,
|
playbackStopInfo: {
|
||||||
mediaSourceId: mediaSourceId,
|
ItemId: item.Id,
|
||||||
positionTicks: currentTimeInTicks,
|
MediaSourceId: mediaSourceId,
|
||||||
playSessionId: stream.sessionId,
|
PositionTicks: currentTimeInTicks,
|
||||||
|
PlaySessionId: stream.sessionId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [api, item, mediaSourceId, stream, progress, offline]);
|
||||||
api,
|
|
||||||
item,
|
|
||||||
mediaSourceId,
|
|
||||||
stream,
|
|
||||||
progress,
|
|
||||||
offline,
|
|
||||||
revalidateProgressCache,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
// Update URL with final playback position before stopping
|
// Update URL with final playback position before stopping
|
||||||
@@ -471,9 +465,10 @@ export default function DirectPlayerPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||||
return () => {
|
return () => {
|
||||||
|
reportPlaybackStopped();
|
||||||
beforeRemoveListener();
|
beforeRemoveListener();
|
||||||
};
|
};
|
||||||
}, [navigation, stop]);
|
}, [navigation, stop, reportPlaybackStopped]);
|
||||||
|
|
||||||
const currentPlayStateInfo = useCallback(():
|
const currentPlayStateInfo = useCallback(():
|
||||||
| PlaybackProgressInfo
|
| PlaybackProgressInfo
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB |
BIN
assets/icons/gearshape.fill.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 22 KiB |
BIN
assets/icons/link.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
BIN
assets/icons/list.star.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/rectangle.stack.fill.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
1
assets/icons/seerr-logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,65 +0,0 @@
|
|||||||
<svg
|
|
||||||
type="certified"
|
|
||||||
viewBox="0 0 80 80"
|
|
||||||
preserveAspectRatio="xMidYMid"
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<g transform="translate(2.29, 0)">
|
|
||||||
<path
|
|
||||||
d="M42.1942857,18.8022857 C44.3794286,18.608 49.1565714,18.7177143 51.4902857,21.0057143 C51.6297143,21.1451429 51.5085714,21.4605714 51.3097143,21.408 C47.8902857,20.4868571 42.5577143,25.0217143 39.1017143,22.0891429 C39.008,22.9485714 38.2331429,27.0857143 32.3314286,26.4731429 C32.192,26.4594286 32.1371429,26.304 32.24,26.2171429 C33.1542857,25.44 34.2765714,23.2891429 33.3142857,21.9154286 C30.3108571,23.9085714 28.7565714,23.9954286 23.2182857,21.5954286 C23.0377143,21.5177143 23.1451429,21.2228571 23.3577143,21.1748571 C24.5074286,20.9165714 27.2434286,19.9222857 29.696,19.4582857 C30.1645714,19.3691429 30.624,19.3165714 31.0674286,19.312 C28.528,18.7062857 27.4217143,18.1805714 25.7485714,18.1874286 C25.5657143,18.1897143 25.4742857,17.9611429 25.6068571,17.8354286 C28.224,15.3188571 32.9691429,15.1885714 35.2548571,17.0628571 L33.2068571,12.7862857 L35.696,12.4114286 C35.696,12.4114286 36.3451429,14.6925714 36.9257143,16.7428571 C39.5177143,13.904 43.5268571,14.192 44.8777143,16.672 C44.9577143,16.8182857 44.8251429,16.992 44.6605714,16.9622857 C43.3005714,16.7314286 42.3702857,17.8628571 42.1737143,18.7977143 L42.1942857,18.8022857"
|
|
||||||
id="Fill-2"
|
|
||||||
fill="#00912D"
|
|
||||||
></path>
|
|
||||||
<mask id="mask-2" fill="white">
|
|
||||||
<polygon
|
|
||||||
points="0.137142857 0.921142857 75.0534777 0.921142857 75.0534777 79.8628571 0.137142857 79.8628571"
|
|
||||||
></polygon>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M13.0491429,59.1817143 C9.90628571,55.3554286 7.86971429,50.576 7.51771429,44.9622857 C6.912,35.2342857 10.2354286,26.0845714 23.1794286,21.4834286 C23.1908571,21.5245714 23.1725714,21.5748571 23.2182857,21.5954286 C23.0377143,21.5177143 23.1451429,21.2228571 23.3577143,21.1748571 C24.5074286,20.9165714 27.2434286,19.92 29.696,19.4582857 C30.1645714,19.3691429 30.624,19.3165714 31.0674286,19.3097143 C28.528,18.7062857 27.4217143,18.1805714 25.7485714,18.1874286 C25.5657143,18.1897143 25.4742857,17.9611429 25.6068571,17.8331429 C28.224,15.3165714 32.9691429,15.1885714 35.2548571,17.0628571 L33.2068571,12.784 L35.696,12.4114286 C35.696,12.4114286 36.3451429,14.6902857 36.9257143,16.7428571 C39.5177143,13.904 43.5268571,14.192 44.8777143,16.672 C44.9577143,16.8182857 44.8251429,16.992 44.6605714,16.9622857 C43.3005714,16.7314286 42.3702857,17.8628571 42.1737143,18.7977143 L42.1942857,18.8022857 C44.3794286,18.608 49.1565714,18.7177143 51.4902857,21.0057143 C51.328,20.8502857 51.1337143,20.7245714 50.9508571,20.5874286 C60.2765714,23.504 66.7474286,30.1531429 67.44,41.2251429 C67.8811429,48.2948571 65.5702857,54.3885714 61.568,59.1154286 C62.784,59.2891429 63.9931429,59.4925714 65.2045714,59.6937143 C70.304,53.4537143 73.2502857,45.5428571 73.2502857,37.056 C73.2502857,17.7165714 57.5337143,2.56685714 37.472,2.56685714 C17.4102857,2.56685714 1.69371429,17.7165714 1.69371429,37.056 C1.69371429,45.5565714 4.64,53.472 9.744,59.7097143 C10.8434286,59.5268571 11.9451429,59.3462857 13.0491429,59.1817143"
|
|
||||||
fill="#FFD700"
|
|
||||||
mask="url(#mask-2)"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M9.744,59.7097143 C4.64,53.472 1.69371429,45.5565714 1.69371429,37.056 C1.69371429,17.7165714 17.4102857,2.56685714 37.472,2.56685714 C57.5337143,2.56685714 73.2502857,17.7165714 73.2502857,37.056 C73.2502857,45.5428571 70.304,53.4537143 65.2045714,59.6937143 C65.8125714,59.7942857 66.4205714,59.8742857 67.0285714,59.984 C71.9497143,53.6457143 74.8937143,45.6982857 74.8937143,37.056 C74.8937143,16.3862857 58.1394286,0.921142857 37.472,0.921142857 C16.8022857,0.921142857 0.048,16.3862857 0.048,37.056 C0.048,45.7074286 2.99885714,53.6594286 7.92914286,59.9977143 C8.53257143,59.8902857 9.13828571,59.8102857 9.744,59.7097143"
|
|
||||||
fill="#FA6E0F"
|
|
||||||
mask="url(#mask-2)"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M58.2857143,74.9394286 C62.3748571,75.1954286 65.7874286,77.2137143 67.8468571,79.9474286 C67.9131429,80.0182857 68.0114286,80.016 68.0411429,79.9382857 C68.7451429,77.0971429 68.9394286,74.0662857 68.5851429,71.0125714 C68.5874286,70.9805714 68.6125714,70.9577143 68.6537143,70.9485714 C70.576,70.3428571 72.7017143,70.0137143 74.9645714,70.0457143 C75.0857143,70.0594286 75.0834286,69.9405714 74.9554286,69.8194286 C72.5577143,67.4994286 69.6297143,65.6914286 66.416,64.5417143 C65.3051429,67.68 64.2217143,70.816 63.1565714,73.9634286 C63.136,74.0228571 63.0514286,74.0594286 62.9645714,74.0434286 L58.2857143,74.9394286"
|
|
||||||
fill="#0AC855"
|
|
||||||
mask="url(#mask-2)"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M62.9645714,74.0434286 L58.2857143,74.9394286 C58.2857143,74.9394286 58.3451429,74.512 58.528,73.3325714 C60.9417143,73.6754286 62.9645714,74.0434286 62.9645714,74.0434286"
|
|
||||||
fill="#0B4902"
|
|
||||||
></path>
|
|
||||||
<g transform="translate(0, 20.57)">
|
|
||||||
<mask id="mask-4" fill="white">
|
|
||||||
<polygon
|
|
||||||
points="0.137142857 0.016 67.4935952 0.016 67.4935952 59.2914286 0.137142857 59.2914286"
|
|
||||||
></polygon>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M13.0765714,38.6057143 C29.1177143,36.2605714 45.5222857,36.2354286 61.568,38.544 C65.5702857,33.8171429 67.8811429,27.7234286 67.44,20.6537143 C66.7474286,9.58171429 60.2765714,2.93257143 50.9508571,0.016 C51.1337143,0.153142857 51.328,0.278857143 51.4902857,0.434285714 C51.6297143,0.573714286 51.5085714,0.889142857 51.3097143,0.836571429 C47.8902857,-0.0845714286 42.5577143,4.45028571 39.1017143,1.51771429 C39.008,2.37485714 38.2331429,6.51428571 32.3314286,5.90171429 C32.192,5.888 32.1371429,5.73257143 32.24,5.64571429 C33.1542857,4.86857143 34.2765714,2.71542857 33.3142857,1.344 C30.3108571,3.33714286 28.7565714,3.424 23.2182857,1.024 C23.1725714,1.00342857 23.1908571,0.953142857 23.1794286,0.912 C10.2354286,5.51314286 6.912,14.6628571 7.51771429,24.3908571 C7.86971429,30.0091429 9.93142857,34.7748571 13.0765714,38.6057143"
|
|
||||||
fill="#FA3200"
|
|
||||||
mask="url(#mask-4)"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M12.0868571,53.472 C12,53.488 11.9154286,53.4514286 11.8948571,53.392 C10.8274286,50.2445714 9.73485714,47.0971429 8.62171429,43.9611429 C5.41028571,45.1108571 2.49371429,46.9302857 0.0982857143,49.248 C-0.0297142857,49.3691429 -0.032,49.488 0.0891428571,49.4742857 C2.352,49.4422857 4.47771429,49.7714286 6.4,50.3771429 C6.44114286,50.3862857 6.46628571,50.4091429 6.46857143,50.4411429 C6.11428571,53.4948571 6.30857143,56.5257143 7.01257143,59.3668571 C7.04228571,59.4445714 7.14057143,59.4468571 7.20685714,59.376 C9.26628571,56.6422857 12.6742857,54.624 16.7657143,54.368 L12.0868571,53.472"
|
|
||||||
fill="#0AC855"
|
|
||||||
mask="url(#mask-4)"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
d="M62.9645714,74.0434286 C46.192,71.104 28.8571429,71.104 12.0868571,74.0434286 C12,74.0594286 11.9154286,74.0228571 11.8948571,73.9634286 C10.3428571,69.3851429 8.74285714,64.8182857 7.09257143,60.2628571 C7.06971429,60.1988571 7.14057143,60.1257143 7.248,60.1074286 C27.1885714,56.464 47.8605714,56.464 67.8034286,60.1074286 C67.9108571,60.1257143 67.9817143,60.1988571 67.9565714,60.2628571 C66.3085714,64.8182857 64.7085714,69.3851429 63.1565714,73.9634286 C63.136,74.0228571 63.0514286,74.0594286 62.9645714,74.0434286"
|
|
||||||
fill="#00912D"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M12.0868571,74.0434286 L16.7657143,74.9394286 C16.7657143,74.9394286 16.704,74.512 16.5211429,73.3325714 C14.1074286,73.6754286 12.0868571,74.0434286 12.0868571,74.0434286"
|
|
||||||
fill="#0B4902"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 50 KiB |
1
assets/images/rt_aud_fresh.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
1
assets/images/rt_aud_rotten.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><g transform="translate(33 140)"><path d="m43.802 267.32l237.94 23.482c4.7937 14.937 11.149 29.517 20.259 40.256l-230.27-30.125c-10.248-6.7519-20.861-19.877-27.936-33.612zm222.88-75.298c0.60053 20.906 3.4316 47.964 8.3462 71.017l-241.15-19.083c-7.518-22.739-9.8466-35.959-11.704-54.885l244.51 2.951zm8.3462-102.71c-4.9146 23.053-7.7456 50.111-8.3462 71.017l-244.51 2.951c1.8576-18.926 4.1862-32.146 11.704-54.885l241.15-19.083zm26.973-68.019c-9.1095 10.74-15.465 25.318-20.259 40.257l-237.94 23.48c7.0751-13.735 17.689-26.859 27.936-33.612l230.27-30.125z" fill="#fff"/><path d="m303.57 264.67c3.155-7.8209 14.337-12.586 22.367-12.028 8.5825 0.59581 17.699 9.6258 19.292 18.507 0.29589-0.32244 0.60578-0.62735 0.92093-0.92701 2.7558-2.6356 6.2084-4.3845 9.9867-4.8664-0.57777-2.562-0.71609-5.3045-0.3204-8.1188 1.3954-9.901 9.3336-17.326 18.422-17.252 5.8652 0.047314 11.011 3.0509 14.364 7.6649 0.29939-0.37501 0.6303-0.71848 0.95245-1.069 3.8343-19.991 6.2644-42.578 6.8177-66.547 1.8996-82.42-18.993-149.75-46.663-150.39-27.672-0.63962-51.644 65.656-53.544 148.08 0 0-1.4654 30.062 7.4042 86.951" fill="#00641E"/><path d="m490.91 354.8c1.6353-2.732 2.5492-6.0072 2.4862-9.4874 0.5305-11.245-7.1819-21.439-17.913-20.31 0.3099-1.2862 0.51299-2.6233 0.59178-4.0024 0.64255-11.264-7.1188-20.972-17.337-21.682-0.2241-0.014019-0.44471-0.021029-0.66706-0.028039 1.0627-2.795 1.5897-5.916 1.4024-9.2228-0.52875-9.3717-6.9858-17.268-15.386-18.822-3.0342-0.56076-5.9773-0.28213-8.6718 0.65188-2.5457-6.277-7.8909-10.933-14.393-11.916-0.51474-10.192-7.8699-18.58-17.34-19.238-5.9545-0.41356-11.426 2.3237-15.085 6.9061-3.3528-4.614-8.4985-7.6158-14.364-7.6632-9.0885-0.075352-17.027 7.3495-18.422 17.252-0.39569 2.8126-0.25737 5.555 0.3204 8.1188-3.7783 0.48015-7.2309 2.2308-9.9867 4.8646-0.31515 0.29966-0.62504 0.60457-0.92093 0.92701-1.5932-8.8811-10.71-17.909-19.292-18.507-8.0293-0.55726-19.357 4.3249-22.367 12.028 1.3201 13.434 9.71 50.053 40.055 82.903l0.26963 0.019276c2.9256 2.6496 6.7459 4.1093 10.818 3.7466 2.5247-0.22606 4.8498-1.1303 6.8527-2.5252l0.48848 0.033295c2.67 1.8558 5.8793 2.8266 9.2706 2.5252 1.2956-0.11566 2.5317-0.42758 3.7065-0.87269 2.9064 6.0142 9.3879 9.901 16.622 9.2631 5.6026-0.49417 10.365-3.5959 13.164-7.9611l0.90692 0.063086c2.7961 2.774 6.513 4.3897 10.522 4.2688 3.3143 5.0188 9.4019 8.1065 16.12 7.516 2.5299-0.22255 4.8918-0.95505 6.998-2.0643 3.5139 4.3266 9.2811 6.8991 15.609 6.3419 6.2557-0.5485 11.54-4.02 14.414-8.8197 2.8241 2.2693 6.3625 3.4872 10.12 3.1525 3.6452-0.32594 6.8842-2.0485 9.3179-4.6543l0.40619 0.028038c0.55326-0.80259 1.026-1.6245 1.4654-2.4533 0.010505-0.015772 0.019259-0.033296 0.028014-0.049067 0.059527-0.1104 0.13306-0.21905 0.18909-0.3312" fill="#FFD700"/><path d="m281.75 61.547l-237.94 23.48c7.0751-13.735 17.689-26.859 27.936-33.612l230.27-30.125c-9.1095 10.74-15.465 25.318-20.259 40.257zm20.259 269.51l-230.27-30.125c-10.248-6.7519-20.861-19.877-27.936-33.611l237.94 23.48c4.7937 14.937 11.149 29.517 20.259 40.256zm-268.13-87.102c-7.518-22.739-9.8466-35.957-11.704-54.885l244.51 2.951c0.60053 20.906 3.4316 47.964 8.3462 71.017l-241.15-19.083zm0-135.56l241.15-19.083c-4.9146 23.053-7.7456 50.111-8.3462 71.019l-244.51 2.9493c1.8576-18.926 4.1862-32.146 11.704-54.885zm344.72-82.679c-15.255-17.778-26.206-26.124-35.587-25.04-1.7491 0.22255-266.89 37.222-266.89 37.222-42.074 4.2828-75.7 65.432-76.117 138.28 0.4167 72.843 34.043 133.99 76.117 138.28 0 0 265.64 37.135 266.89 37.221 2.2183-0.014019 4.4086-0.31192 6.5673-0.86568-2.101-0.6256-4.0391-1.7208-5.6867-3.2139l-0.26963-0.019276c-30.345-32.848-38.735-69.47-40.055-82.903 0.003501-0.010514 0.010505-0.019276 0.014006-0.02979-0.003501 0.010514-0.010505 0.019276-0.014006 0.02979-8.8697-56.889-7.4042-86.951-7.4042-86.951 1.8996-82.42 25.872-148.72 53.544-148.08 27.67 0.63962 48.562 67.973 46.663 150.39-0.55326 23.969-2.9834 46.556-6.8177 66.547 3.9393-4.3512 9.2233-6.1842 14.133-5.8372 0.90342 0.064838 1.7823 0.2173 2.6437 0.41531 16.804-92.03-0.81238-181.89-27.729-215.44z" fill="#04A53C"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
1
assets/images/rt_fresh.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path d="m478.29 296.98c-3.99-63.966-36.52-111.82-85.468-138.58 0.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.28 7.584 0.285 8.519-1.378 50.072-59.914 52.483-1.382 0.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.26 6.78 108.74 108.63 170.89 211.19 164.49 102.56-6.395 193.47-80.572 186.68-189.31" fill="#FA320A"/><path d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023" fill="#00912D"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
assets/images/rt_rotten.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878" fill="#0AC855"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
assets/images/tmdb_logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
21
bun.lock
@@ -31,7 +31,6 @@
|
|||||||
"expo-brightness": "~56.0.5",
|
"expo-brightness": "~56.0.5",
|
||||||
"expo-build-properties": "~56.0.15",
|
"expo-build-properties": "~56.0.15",
|
||||||
"expo-camera": "~56.0.7",
|
"expo-camera": "~56.0.7",
|
||||||
"expo-clipboard": "~56.0.3",
|
|
||||||
"expo-constants": "~56.0.16",
|
"expo-constants": "~56.0.16",
|
||||||
"expo-crypto": "~56.0.4",
|
"expo-crypto": "~56.0.4",
|
||||||
"expo-dev-client": "~56.0.16",
|
"expo-dev-client": "~56.0.16",
|
||||||
@@ -104,10 +103,8 @@
|
|||||||
"@biomejs/biome": "2.4.16",
|
"@biomejs/biome": "2.4.16",
|
||||||
"@react-native-community/cli": "20.1.3",
|
"@react-native-community/cli": "20.1.3",
|
||||||
"@react-native-tvos/config-tv": "0.1.6",
|
"@react-native-tvos/config-tv": "0.1.6",
|
||||||
"@types/bun": "^1.3.14",
|
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lodash": "4.17.24",
|
"@types/lodash": "4.17.24",
|
||||||
"@types/node": "^24",
|
|
||||||
"@types/react": "~19.2.10",
|
"@types/react": "~19.2.10",
|
||||||
"@types/react-test-renderer": "19.1.0",
|
"@types/react-test-renderer": "19.1.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
@@ -590,8 +587,6 @@
|
|||||||
|
|
||||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
|
||||||
|
|
||||||
"@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="],
|
"@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="],
|
||||||
|
|
||||||
"@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="],
|
"@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="],
|
||||||
@@ -608,7 +603,7 @@
|
|||||||
|
|
||||||
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.13.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-5vtOqGQr4NJKeEzV441FcOi2MeG9UTWq9LqVLGneDdu4vlX17H8kQ2PA2UmNwCUGPVDj4oBjNhS7ReVEIWJJrg=="],
|
"@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
|
"@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
|
||||||
|
|
||||||
@@ -748,8 +743,6 @@
|
|||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
||||||
@@ -962,8 +955,6 @@
|
|||||||
|
|
||||||
"expo-camera": ["expo-camera@56.0.7", "", { "dependencies": { "barcode-detector": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-c8z+UheidFintQyP9XLEDP43aK4PS/o9+TFLW0zEOjdqkYCBgoWq6Mw/Ps62kjBeftFY7xrp5ZLITbenNvbTaw=="],
|
"expo-camera": ["expo-camera@56.0.7", "", { "dependencies": { "barcode-detector": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-c8z+UheidFintQyP9XLEDP43aK4PS/o9+TFLW0zEOjdqkYCBgoWq6Mw/Ps62kjBeftFY7xrp5ZLITbenNvbTaw=="],
|
||||||
|
|
||||||
"expo-clipboard": ["expo-clipboard@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-8mCdhmAomm0yBIonJFjAhKUXvSkc2avdNh4+rBwoe7DSWF2AC4w3uy+pa419rvVFbTyVxOBmh83UHAbUwD6qAg=="],
|
|
||||||
|
|
||||||
"expo-constants": ["expo-constants@56.0.16", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA=="],
|
"expo-constants": ["expo-constants@56.0.16", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA=="],
|
||||||
|
|
||||||
"expo-crypto": ["expo-crypto@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-fRNEhoXRXgAWBpe3/hq5X+KXTit3OZqdiAGts1YvNEUHQb+H5591mpPac0Yw+sZg9pXcrjRnzo5AxvZaENpc7g=="],
|
"expo-crypto": ["expo-crypto@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-fRNEhoXRXgAWBpe3/hq5X+KXTit3OZqdiAGts1YvNEUHQb+H5591mpPac0Yw+sZg9pXcrjRnzo5AxvZaENpc7g=="],
|
||||||
@@ -1836,7 +1827,7 @@
|
|||||||
|
|
||||||
"ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="],
|
"ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
|
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
|
||||||
|
|
||||||
@@ -2040,8 +2031,6 @@
|
|||||||
|
|
||||||
"brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
|
||||||
|
|
||||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -2140,8 +2129,6 @@
|
|||||||
|
|
||||||
"nativewind/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"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=="],
|
|
||||||
|
|
||||||
"npm-package-arg/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
"npm-package-arg/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
||||||
|
|
||||||
"parse-png/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
|
"parse-png/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
|
||||||
@@ -2260,8 +2247,6 @@
|
|||||||
|
|
||||||
"ansi-fragments/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
"ansi-fragments/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
|
||||||
|
|
||||||
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"chrome-launcher/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"chrome-launcher/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
@@ -2318,8 +2303,6 @@
|
|||||||
|
|
||||||
"metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
|
||||||
|
|
||||||
"patch-package/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
"patch-package/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
|
|
||||||
"patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
"patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|||||||
@@ -89,14 +89,14 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<View className='flex flex-row items-center mt-4'>
|
<View className='flex flex-row items-center mt-4'>
|
||||||
<Image
|
<Image
|
||||||
source={require("@/assets/icons/jellyseerr-logo.svg")}
|
source={require("@/assets/icons/seerr-logo.svg")}
|
||||||
style={{
|
style={{
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View className='shrink ml-2'>
|
<View className='shrink ml-2'>
|
||||||
<Text className='font-bold mb-1'>Jellyseerr</Text>
|
<Text className='font-bold mb-1'>Seerr</Text>
|
||||||
<Text className='shrink text-xs'>
|
<Text className='shrink text-xs'>
|
||||||
{t("home.intro.jellyseerr_feature_description")}
|
{t("home.intro.jellyseerr_feature_description")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export const Ratings: React.FC<Props> = ({ item, ...props }) => {
|
|||||||
<Image
|
<Image
|
||||||
source={
|
source={
|
||||||
item.CriticRating < 60
|
item.CriticRating < 60
|
||||||
? require("@/assets/images/rotten-tomatoes.png")
|
? require("@/assets/images/rt_rotten.svg")
|
||||||
: require("@/assets/images/not-rotten-tomatoes.svg")
|
: require("@/assets/images/rt_fresh.svg")
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
width: 14,
|
width: 14,
|
||||||
@@ -89,8 +89,8 @@ export const JellyserrRatings: React.FC<{
|
|||||||
className='mr-1'
|
className='mr-1'
|
||||||
source={
|
source={
|
||||||
data?.criticsRating === "Rotten"
|
data?.criticsRating === "Rotten"
|
||||||
? require("@/utils/jellyseerr/src/assets/rt_rotten.svg")
|
? require("@/assets/images/rt_rotten.svg")
|
||||||
: require("@/utils/jellyseerr/src/assets/rt_fresh.svg")
|
: require("@/assets/images/rt_fresh.svg")
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
width: 14,
|
width: 14,
|
||||||
@@ -109,8 +109,8 @@ export const JellyserrRatings: React.FC<{
|
|||||||
className='mr-1'
|
className='mr-1'
|
||||||
source={
|
source={
|
||||||
data?.audienceRating === "Spilled"
|
data?.audienceRating === "Spilled"
|
||||||
? require("@/utils/jellyseerr/src/assets/rt_aud_rotten.svg")
|
? require("@/assets/images/rt_aud_rotten.svg")
|
||||||
: require("@/utils/jellyseerr/src/assets/rt_aud_fresh.svg")
|
: require("@/assets/images/rt_aud_fresh.svg")
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
width: 14,
|
width: 14,
|
||||||
@@ -127,7 +127,7 @@ export const JellyserrRatings: React.FC<{
|
|||||||
iconLeft={
|
iconLeft={
|
||||||
<Image
|
<Image
|
||||||
className='mr-1'
|
className='mr-1'
|
||||||
source={require("@/utils/jellyseerr/src/assets/tmdb_logo.svg")}
|
source={require("@/assets/images/tmdb_logo.svg")}
|
||||||
style={{
|
style={{
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
|
|||||||
@@ -34,13 +34,12 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const effectiveSubtitle = disabledByAdmin ? "Disabled by admin" : subtitle;
|
const effectiveSubtitle = disabledByAdmin ? "Disabled by admin" : subtitle;
|
||||||
const isDisabled = disabled || disabledByAdmin;
|
const isDisabled = disabled || disabledByAdmin;
|
||||||
const hasSubtitle = Boolean(effectiveSubtitle);
|
|
||||||
if (onPress)
|
if (onPress)
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 ${hasSubtitle ? "min-h-[48px] py-2" : "h-[48px]"} px-4 ${isDisabled ? "opacity-50" : ""}`}
|
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
||||||
{...(viewProps as any)}
|
{...(viewProps as any)}
|
||||||
>
|
>
|
||||||
<ListItemContent
|
<ListItemContent
|
||||||
@@ -59,7 +58,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className={`flex flex-row items-center justify-between bg-neutral-900 ${hasSubtitle ? "min-h-[48px] py-2" : "h-[48px]"} px-4 ${isDisabled ? "opacity-50" : ""}`}
|
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${isDisabled ? "opacity-50" : ""}`}
|
||||||
{...viewProps}
|
{...viewProps}
|
||||||
>
|
>
|
||||||
<ListItemContent
|
<ListItemContent
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { Switch, View } from "react-native";
|
||||||
import { View } from "react-native";
|
|
||||||
import { SettingsSwitchRow } from "@/components/settings/index/SettingsSwitchRow";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const ChromecastSettings: React.FC = ({ ...props }) => {
|
export const ChromecastSettings: React.FC = ({ ...props }) => {
|
||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={t("home.settings.chromecast.title")}>
|
<ListGroup title={"Chromecast"}>
|
||||||
<SettingsSwitchRow
|
<ListItem title={"Enable H265 for Chromecast"}>
|
||||||
title={t("home.settings.chromecast.enable_h265")}
|
<Switch
|
||||||
value={settings.enableH265ForChromecast}
|
value={settings.enableH265ForChromecast}
|
||||||
onValueChange={(enableH265ForChromecast) =>
|
onValueChange={(enableH265ForChromecast) =>
|
||||||
updateSettings({ enableH265ForChromecast })
|
updateSettings({ enableH265ForChromecast })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import type React from "react";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ViewProps } from "react-native";
|
import type { ViewProps } from "react-native";
|
||||||
|
import { Switch } from "react-native";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import { SettingsSwitchRow } from "@/components/settings/index/SettingsSwitchRow";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -31,65 +32,85 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
|||||||
<ListGroup
|
<ListGroup
|
||||||
title={t("home.settings.gesture_controls.gesture_controls_title")}
|
title={t("home.settings.gesture_controls.gesture_controls_title")}
|
||||||
>
|
>
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.gesture_controls.horizontal_swipe_skip")}
|
title={t("home.settings.gesture_controls.horizontal_swipe_skip")}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
"home.settings.gesture_controls.horizontal_swipe_skip_description",
|
"home.settings.gesture_controls.horizontal_swipe_skip_description",
|
||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
||||||
value={settings.enableHorizontalSwipeSkip}
|
>
|
||||||
onValueChange={(enableHorizontalSwipeSkip) =>
|
<Switch
|
||||||
updateSettings({ enableHorizontalSwipeSkip })
|
value={settings.enableHorizontalSwipeSkip}
|
||||||
}
|
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
|
||||||
/>
|
onValueChange={(enableHorizontalSwipeSkip) =>
|
||||||
|
updateSettings({ enableHorizontalSwipeSkip })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.gesture_controls.left_side_brightness")}
|
title={t("home.settings.gesture_controls.left_side_brightness")}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
"home.settings.gesture_controls.left_side_brightness_description",
|
"home.settings.gesture_controls.left_side_brightness_description",
|
||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
||||||
value={settings.enableLeftSideBrightnessSwipe}
|
>
|
||||||
onValueChange={(enableLeftSideBrightnessSwipe) =>
|
<Switch
|
||||||
updateSettings({ enableLeftSideBrightnessSwipe })
|
value={settings.enableLeftSideBrightnessSwipe}
|
||||||
}
|
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
|
||||||
/>
|
onValueChange={(enableLeftSideBrightnessSwipe) =>
|
||||||
|
updateSettings({ enableLeftSideBrightnessSwipe })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.gesture_controls.right_side_volume")}
|
title={t("home.settings.gesture_controls.right_side_volume")}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
"home.settings.gesture_controls.right_side_volume_description",
|
"home.settings.gesture_controls.right_side_volume_description",
|
||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
||||||
value={settings.enableRightSideVolumeSwipe}
|
>
|
||||||
onValueChange={(enableRightSideVolumeSwipe) =>
|
<Switch
|
||||||
updateSettings({ enableRightSideVolumeSwipe })
|
value={settings.enableRightSideVolumeSwipe}
|
||||||
}
|
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
|
||||||
/>
|
onValueChange={(enableRightSideVolumeSwipe) =>
|
||||||
|
updateSettings({ enableRightSideVolumeSwipe })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.gesture_controls.hide_volume_slider")}
|
title={t("home.settings.gesture_controls.hide_volume_slider")}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
"home.settings.gesture_controls.hide_volume_slider_description",
|
"home.settings.gesture_controls.hide_volume_slider_description",
|
||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
||||||
value={settings.hideVolumeSlider}
|
>
|
||||||
onValueChange={(hideVolumeSlider) =>
|
<Switch
|
||||||
updateSettings({ hideVolumeSlider })
|
value={settings.hideVolumeSlider}
|
||||||
}
|
disabled={pluginSettings?.hideVolumeSlider?.locked}
|
||||||
/>
|
onValueChange={(hideVolumeSlider) =>
|
||||||
|
updateSettings({ hideVolumeSlider })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.gesture_controls.hide_brightness_slider")}
|
title={t("home.settings.gesture_controls.hide_brightness_slider")}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
"home.settings.gesture_controls.hide_brightness_slider_description",
|
"home.settings.gesture_controls.hide_brightness_slider_description",
|
||||||
)}
|
)}
|
||||||
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||||
value={settings.hideBrightnessSlider}
|
>
|
||||||
onValueChange={(hideBrightnessSlider) =>
|
<Switch
|
||||||
updateSettings({ hideBrightnessSlider })
|
value={settings.hideBrightnessSlider}
|
||||||
}
|
disabled={pluginSettings?.hideBrightnessSlider?.locked}
|
||||||
/>
|
onValueChange={(hideBrightnessSlider) =>
|
||||||
|
updateSettings({ hideBrightnessSlider })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import type React from "react";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ViewProps } from "react-native";
|
import type { ViewProps } from "react-native";
|
||||||
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import { SettingsStepperRow } from "@/components/settings/index/SettingsStepperRow";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -26,27 +27,35 @@ export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={disabled} {...props}>
|
<DisabledSetting disabled={disabled} {...props}>
|
||||||
<ListGroup title={t("home.settings.media_controls.media_controls_title")}>
|
<ListGroup title={t("home.settings.media_controls.media_controls_title")}>
|
||||||
<SettingsStepperRow
|
<ListItem
|
||||||
title={t("home.settings.media_controls.forward_skip_length")}
|
title={t("home.settings.media_controls.forward_skip_length")}
|
||||||
disabled={pluginSettings?.forwardSkipTime?.locked}
|
disabled={pluginSettings?.forwardSkipTime?.locked}
|
||||||
value={settings.forwardSkipTime}
|
>
|
||||||
step={5}
|
<Stepper
|
||||||
appendValue={t("home.settings.media_controls.seconds_unit")}
|
value={settings.forwardSkipTime}
|
||||||
min={0}
|
disabled={pluginSettings?.forwardSkipTime?.locked}
|
||||||
max={60}
|
step={5}
|
||||||
onUpdate={(forwardSkipTime) => updateSettings({ forwardSkipTime })}
|
appendValue={t("home.settings.media_controls.seconds_unit")}
|
||||||
/>
|
min={0}
|
||||||
|
max={60}
|
||||||
|
onUpdate={(forwardSkipTime) => updateSettings({ forwardSkipTime })}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsStepperRow
|
<ListItem
|
||||||
title={t("home.settings.media_controls.rewind_length")}
|
title={t("home.settings.media_controls.rewind_length")}
|
||||||
disabled={pluginSettings?.rewindSkipTime?.locked}
|
disabled={pluginSettings?.rewindSkipTime?.locked}
|
||||||
value={settings.rewindSkipTime}
|
>
|
||||||
step={5}
|
<Stepper
|
||||||
appendValue={t("home.settings.media_controls.seconds_unit")}
|
value={settings.rewindSkipTime}
|
||||||
min={0}
|
disabled={pluginSettings?.rewindSkipTime?.locked}
|
||||||
max={60}
|
step={5}
|
||||||
onUpdate={(rewindSkipTime) => updateSettings({ rewindSkipTime })}
|
appendValue={t("home.settings.media_controls.seconds_unit")}
|
||||||
/>
|
min={0}
|
||||||
|
max={60}
|
||||||
|
onUpdate={(rewindSkipTime) => updateSettings({ rewindSkipTime })}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SettingsSelectRow } from "@/components/settings/index/SettingsSelectRow";
|
import { View } from "react-native";
|
||||||
import { SettingsStepperRow } from "@/components/settings/index/SettingsStepperRow";
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import { type MpvCacheMode, useSettings } from "@/utils/atoms/settings";
|
import { type MpvCacheMode, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
const CACHE_MODE_OPTIONS: { key: string; value: MpvCacheMode }[] = [
|
const CACHE_MODE_OPTIONS: { key: string; value: MpvCacheMode }[] = [
|
||||||
{ key: "home.settings.buffer.cache_auto", value: "auto" },
|
{ key: "home.settings.buffer.cache_auto", value: "auto" },
|
||||||
@@ -41,43 +45,56 @@ export const MpvBufferSettings: React.FC = () => {
|
|||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListGroup title={t("home.settings.buffer.title")}>
|
<ListGroup title={t("home.settings.buffer.title")} className='mb-4'>
|
||||||
<SettingsSelectRow
|
<ListItem title={t("home.settings.buffer.cache_mode")}>
|
||||||
title={t("home.settings.buffer.cache_mode")}
|
<PlatformDropdown
|
||||||
valueLabel={currentCacheModeLabel}
|
groups={cacheModeOptions}
|
||||||
groups={cacheModeOptions}
|
trigger={
|
||||||
dropdownTitle={t("home.settings.buffer.cache_mode")}
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
/>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{currentCacheModeLabel}
|
||||||
|
</Text>
|
||||||
|
<Ionicons name='chevron-expand-sharp' size={18} color='#5A5960' />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.buffer.cache_mode")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsStepperRow
|
<ListItem title={t("home.settings.buffer.buffer_duration")}>
|
||||||
title={t("home.settings.buffer.buffer_duration")}
|
<Stepper
|
||||||
value={settings.mpvCacheSeconds ?? 10}
|
value={settings.mpvCacheSeconds ?? 10}
|
||||||
step={5}
|
step={5}
|
||||||
min={5}
|
min={5}
|
||||||
max={120}
|
max={120}
|
||||||
onUpdate={(value) => updateSettings({ mpvCacheSeconds: value })}
|
onUpdate={(value) => updateSettings({ mpvCacheSeconds: value })}
|
||||||
appendValue='s'
|
appendValue='s'
|
||||||
/>
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsStepperRow
|
<ListItem title={t("home.settings.buffer.max_cache_size")}>
|
||||||
title={t("home.settings.buffer.max_cache_size")}
|
<Stepper
|
||||||
value={settings.mpvDemuxerMaxBytes ?? 150}
|
value={settings.mpvDemuxerMaxBytes ?? 150}
|
||||||
step={25}
|
step={25}
|
||||||
min={50}
|
min={50}
|
||||||
max={500}
|
max={500}
|
||||||
onUpdate={(value) => updateSettings({ mpvDemuxerMaxBytes: value })}
|
onUpdate={(value) => updateSettings({ mpvDemuxerMaxBytes: value })}
|
||||||
appendValue=' MB'
|
appendValue=' MB'
|
||||||
/>
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsStepperRow
|
<ListItem title={t("home.settings.buffer.max_backward_cache")}>
|
||||||
title={t("home.settings.buffer.max_backward_cache")}
|
<Stepper
|
||||||
value={settings.mpvDemuxerMaxBackBytes ?? 50}
|
value={settings.mpvDemuxerMaxBackBytes ?? 50}
|
||||||
step={25}
|
step={25}
|
||||||
min={25}
|
min={25}
|
||||||
max={200}
|
max={200}
|
||||||
onUpdate={(value) => updateSettings({ mpvDemuxerMaxBackBytes: value })}
|
onUpdate={(value) =>
|
||||||
appendValue=' MB'
|
updateSettings({ mpvDemuxerMaxBackBytes: value })
|
||||||
/>
|
}
|
||||||
|
appendValue=' MB'
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { SettingsSelectRow } from "@/components/settings/index/SettingsSelectRow";
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import { type MpvVoDriver, useSettings } from "@/utils/atoms/settings";
|
import { type MpvVoDriver, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
const VO_DRIVER_OPTIONS: { key: string; value: MpvVoDriver }[] = [
|
const VO_DRIVER_OPTIONS: { key: string; value: MpvVoDriver }[] = [
|
||||||
{ key: "home.settings.vo_driver.gpu_next", value: "gpu-next" },
|
{ key: "home.settings.vo_driver.gpu_next", value: "gpu-next" },
|
||||||
@@ -43,13 +46,21 @@ export const MpvVoSettings: React.FC = () => {
|
|||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListGroup title={t("home.settings.vo_driver.title")}>
|
<ListGroup title={t("home.settings.vo_driver.title")} className='mb-4'>
|
||||||
<SettingsSelectRow
|
<ListItem title={t("home.settings.vo_driver.vo_mode")}>
|
||||||
title={t("home.settings.vo_driver.vo_mode")}
|
<PlatformDropdown
|
||||||
valueLabel={currentVoDriverLabel}
|
groups={voDriverOptions}
|
||||||
groups={voDriverOptions}
|
trigger={
|
||||||
dropdownTitle={t("home.settings.vo_driver.vo_mode")}
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
/>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{currentVoDriverLabel}
|
||||||
|
</Text>
|
||||||
|
<Ionicons name='chevron-expand-sharp' size={18} color='#5A5960' />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.vo_driver.vo_mode")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Switch, View } from "react-native";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import { SettingsSelectRow } from "@/components/settings/index/SettingsSelectRow";
|
|
||||||
import { SettingsSwitchRow } from "@/components/settings/index/SettingsSwitchRow";
|
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const PlaybackControlsSettings: React.FC = () => {
|
export const PlaybackControlsSettings: React.FC = () => {
|
||||||
const { settings, updateSettings, pluginSettings } = useSettings();
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
@@ -113,77 +116,141 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={disabled}>
|
<DisabledSetting disabled={disabled}>
|
||||||
<ListGroup title={t("home.settings.other.other_title")} className=''>
|
<ListGroup title={t("home.settings.other.other_title")} className=''>
|
||||||
<SettingsSelectRow
|
<ListItem
|
||||||
title={t("home.settings.other.video_orientation")}
|
title={t("home.settings.other.video_orientation")}
|
||||||
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
||||||
valueLabel={
|
>
|
||||||
t(
|
<PlatformDropdown
|
||||||
orientationTranslations[
|
groups={orientationOptions}
|
||||||
settings.defaultVideoOrientation as keyof typeof orientationTranslations
|
trigger={
|
||||||
],
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
) || "Unknown Orientation"
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
}
|
{t(
|
||||||
groups={orientationOptions}
|
orientationTranslations[
|
||||||
dropdownTitle={t("home.settings.other.orientation")}
|
settings.defaultVideoOrientation as keyof typeof orientationTranslations
|
||||||
/>
|
],
|
||||||
|
) || "Unknown Orientation"}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.orientation")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.other.safe_area_in_controls")}
|
title={t("home.settings.other.safe_area_in_controls")}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
value={settings.safeAreaInControlsEnabled}
|
>
|
||||||
onValueChange={(value) =>
|
<Switch
|
||||||
updateSettings({ safeAreaInControlsEnabled: value })
|
value={settings.safeAreaInControlsEnabled}
|
||||||
}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
/>
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ safeAreaInControlsEnabled: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSelectRow
|
<ListItem
|
||||||
title={t("home.settings.other.default_quality")}
|
title={t("home.settings.other.default_quality")}
|
||||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||||
valueLabel={settings.defaultBitrate?.key}
|
>
|
||||||
groups={bitrateOptions}
|
<PlatformDropdown
|
||||||
dropdownTitle={t("home.settings.other.default_quality")}
|
groups={bitrateOptions}
|
||||||
/>
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between pl-3 py-1.5 '>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{settings.defaultBitrate?.key}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.default_quality")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSelectRow
|
<ListItem
|
||||||
title={t("home.settings.other.default_playback_speed")}
|
title={t("home.settings.other.default_playback_speed")}
|
||||||
disabled={pluginSettings?.defaultPlaybackSpeed?.locked}
|
disabled={pluginSettings?.defaultPlaybackSpeed?.locked}
|
||||||
valueLabel={
|
>
|
||||||
PLAYBACK_SPEEDS.find(
|
<PlatformDropdown
|
||||||
(s) => s.value === settings.defaultPlaybackSpeed,
|
groups={playbackSpeedOptions}
|
||||||
)?.label ?? "1x"
|
trigger={
|
||||||
}
|
<View className='flex flex-row items-center justify-between pl-3 py-1.5'>
|
||||||
groups={playbackSpeedOptions}
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
dropdownTitle={t("home.settings.other.default_playback_speed")}
|
{PLAYBACK_SPEEDS.find(
|
||||||
/>
|
(s) => s.value === settings.defaultPlaybackSpeed,
|
||||||
|
)?.label ?? "1x"}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.default_playback_speed")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.other.disable_haptic_feedback")}
|
title={t("home.settings.other.disable_haptic_feedback")}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
value={settings.disableHapticFeedback}
|
>
|
||||||
onValueChange={(disableHapticFeedback) =>
|
<Switch
|
||||||
updateSettings({ disableHapticFeedback })
|
value={settings.disableHapticFeedback}
|
||||||
}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
/>
|
onValueChange={(disableHapticFeedback) =>
|
||||||
|
updateSettings({ disableHapticFeedback })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSwitchRow
|
<ListItem
|
||||||
title={t("home.settings.other.auto_play_next_episode")}
|
title={t("home.settings.other.auto_play_next_episode")}
|
||||||
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||||
value={settings.autoPlayNextEpisode}
|
>
|
||||||
onValueChange={(autoPlayNextEpisode) =>
|
<Switch
|
||||||
updateSettings({ autoPlayNextEpisode })
|
value={settings.autoPlayNextEpisode}
|
||||||
}
|
disabled={pluginSettings?.autoPlayNextEpisode?.locked}
|
||||||
/>
|
onValueChange={(autoPlayNextEpisode) =>
|
||||||
|
updateSettings({ autoPlayNextEpisode })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<SettingsSelectRow
|
<ListItem
|
||||||
title={t("home.settings.other.max_auto_play_episode_count")}
|
title={t("home.settings.other.max_auto_play_episode_count")}
|
||||||
disabled={
|
disabled={
|
||||||
!settings.autoPlayNextEpisode ||
|
!settings.autoPlayNextEpisode ||
|
||||||
pluginSettings?.maxAutoPlayEpisodeCount?.locked
|
pluginSettings?.maxAutoPlayEpisodeCount?.locked
|
||||||
}
|
}
|
||||||
valueLabel={t(settings?.maxAutoPlayEpisodeCount.key)}
|
>
|
||||||
groups={autoPlayEpisodeOptions}
|
<PlatformDropdown
|
||||||
dropdownTitle={t("home.settings.other.max_auto_play_episode_count")}
|
groups={autoPlayEpisodeOptions}
|
||||||
/>
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(settings?.maxAutoPlayEpisodeCount.key)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.max_auto_play_episode_count")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,57 +1,54 @@
|
|||||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
BottomSheetBackdrop,
|
||||||
useCallback,
|
type BottomSheetBackdropProps,
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Alert, Platform, View } from "react-native";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import {
|
|
||||||
type BottomSheetMethods,
|
|
||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
BottomSheetView,
|
BottomSheetView,
|
||||||
} from "@/utils/expoUiBottomSheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
|
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import type React from "react";
|
||||||
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Alert, Platform, View, type ViewProps } from "react-native";
|
||||||
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { PinInput } from "../inputs/PinInput";
|
import { PinInput } from "../inputs/PinInput";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export type QuickConnectSheetRef = { present: () => void };
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const QuickConnectSheet = forwardRef<QuickConnectSheetRef>(
|
export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||||
(_props, ref) => {
|
const isTv = Platform.isTV;
|
||||||
const isTv = Platform.isTV;
|
const [api] = useAtom(apiAtom);
|
||||||
const [api] = useAtom(apiAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
||||||
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
const modalRef = useRef<BottomSheetMethods>(null);
|
const successHapticFeedback = useHaptic("success");
|
||||||
const successHapticFeedback = useHaptic("success");
|
const errorHapticFeedback = useHaptic("error");
|
||||||
const errorHapticFeedback = useHaptic("error");
|
const snapPoints = useMemo(
|
||||||
const snapPoints = useMemo(
|
() => (Platform.OS === "android" ? ["100%"] : ["40%"]),
|
||||||
() => (Platform.OS === "android" ? ["100%"] : ["40%"]),
|
[],
|
||||||
[],
|
);
|
||||||
);
|
const isAndroid = Platform.OS === "android";
|
||||||
const isAndroid = Platform.OS === "android";
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useImperativeHandle(
|
const { t } = useTranslation();
|
||||||
ref,
|
|
||||||
() => ({
|
|
||||||
present: () => {
|
|
||||||
setQuickConnectCode("");
|
|
||||||
modalRef.current?.present();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const authorizeQuickConnect = useCallback(async () => {
|
const renderBackdrop = useCallback(
|
||||||
if (!quickConnectCode) return;
|
(props: BottomSheetBackdropProps) => (
|
||||||
|
<BottomSheetBackdrop
|
||||||
|
{...props}
|
||||||
|
disappearsOnIndex={-1}
|
||||||
|
appearsOnIndex={0}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const authorizeQuickConnect = useCallback(async () => {
|
||||||
|
if (quickConnectCode) {
|
||||||
try {
|
try {
|
||||||
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
||||||
code: quickConnectCode,
|
code: quickConnectCode,
|
||||||
@@ -64,7 +61,7 @@ export const QuickConnectSheet = forwardRef<QuickConnectSheetRef>(
|
|||||||
t("home.settings.quick_connect.quick_connect_autorized"),
|
t("home.settings.quick_connect.quick_connect_autorized"),
|
||||||
);
|
);
|
||||||
setQuickConnectCode(undefined);
|
setQuickConnectCode(undefined);
|
||||||
modalRef.current?.close();
|
bottomSheetModalRef?.current?.close();
|
||||||
} else {
|
} else {
|
||||||
errorHapticFeedback();
|
errorHapticFeedback();
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
@@ -79,26 +76,39 @@ export const QuickConnectSheet = forwardRef<QuickConnectSheetRef>(
|
|||||||
t("home.settings.quick_connect.invalid_code"),
|
t("home.settings.quick_connect.invalid_code"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}
|
||||||
api,
|
}, [api, user, quickConnectCode]);
|
||||||
user,
|
|
||||||
quickConnectCode,
|
|
||||||
t,
|
|
||||||
successHapticFeedback,
|
|
||||||
errorHapticFeedback,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isTv) return null;
|
if (isTv) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<ListGroup title={t("home.settings.quick_connect.quick_connect_title")}>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => {
|
||||||
|
// Reset the code when opening the sheet
|
||||||
|
setQuickConnectCode("");
|
||||||
|
bottomSheetModalRef?.current?.present();
|
||||||
|
}}
|
||||||
|
title={t("home.settings.quick_connect.authorize_button")}
|
||||||
|
textColor='blue'
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
|
||||||
return (
|
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
ref={modalRef}
|
ref={bottomSheetModalRef}
|
||||||
enablePanDownToClose
|
|
||||||
snapPoints={snapPoints}
|
snapPoints={snapPoints}
|
||||||
handleIndicatorStyle={{ backgroundColor: "white" }}
|
handleIndicatorStyle={{
|
||||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
backgroundStyle={{
|
||||||
|
backgroundColor: "#171717",
|
||||||
|
}}
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
|
keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
|
||||||
keyboardBlurBehavior='restore'
|
keyboardBlurBehavior='restore'
|
||||||
|
android_keyboardInputMode='adjustResize'
|
||||||
|
topInset={isAndroid ? 0 : undefined}
|
||||||
>
|
>
|
||||||
<BottomSheetView>
|
<BottomSheetView>
|
||||||
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
|
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
|
||||||
@@ -132,8 +142,6 @@ export const QuickConnectSheet = forwardRef<QuickConnectSheetRef>(
|
|||||||
</View>
|
</View>
|
||||||
</BottomSheetView>
|
</BottomSheetView>
|
||||||
</BottomSheetModal>
|
</BottomSheetModal>
|
||||||
);
|
</View>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
QuickConnectSheet.displayName = "QuickConnectSheet";
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import type React from "react";
|
|
||||||
import { TouchableOpacity, View } from "react-native";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { getUserImageUrl } from "@/utils/jellyfin/image/getUserImageUrl";
|
|
||||||
|
|
||||||
export const SettingsHero: React.FC<{ onPress: () => void }> = ({
|
|
||||||
onPress,
|
|
||||||
}) => {
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const user = useAtomValue(userAtom);
|
|
||||||
const connected = Boolean(api && user);
|
|
||||||
const imageUrl =
|
|
||||||
api && user?.Id
|
|
||||||
? (getUserImageUrl({
|
|
||||||
serverAddress: api.basePath,
|
|
||||||
userId: user.Id,
|
|
||||||
primaryImageTag: user.PrimaryImageTag,
|
|
||||||
}) ?? undefined)
|
|
||||||
: undefined;
|
|
||||||
const host = api?.basePath?.replace(/^https?:\/\//, "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPress}
|
|
||||||
className='mx-3 mb-4 rounded-2xl overflow-hidden'
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={["#241b33", "#15151a"]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
>
|
|
||||||
<View className='flex-row items-center p-4'>
|
|
||||||
{imageUrl ? (
|
|
||||||
<Image
|
|
||||||
source={{ uri: imageUrl }}
|
|
||||||
style={{ width: 52, height: 52, borderRadius: 26 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<LinearGradient
|
|
||||||
colors={["#a855f7", "#6d28d9"]}
|
|
||||||
style={{
|
|
||||||
width: 52,
|
|
||||||
height: 52,
|
|
||||||
borderRadius: 26,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className='text-white text-[22px] font-bold'>
|
|
||||||
{(user?.Name?.[0] ?? "?").toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
)}
|
|
||||||
<View className='flex-1 ml-3'>
|
|
||||||
<Text
|
|
||||||
className='text-white text-[18px] font-bold'
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{user?.Name ?? ""}
|
|
||||||
</Text>
|
|
||||||
<View className='flex-row items-center mt-0.5'>
|
|
||||||
<View
|
|
||||||
className='w-2 h-2 rounded-full mr-1.5'
|
|
||||||
style={{ backgroundColor: connected ? "#30D158" : "#8E8D91" }}
|
|
||||||
/>
|
|
||||||
<Text className='text-[#9899A1] text-[13px]' numberOfLines={1}>
|
|
||||||
{host}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Ionicons name='chevron-forward' size={18} color='#5A5960' />
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import type React from "react";
|
|
||||||
import { TouchableOpacity, View } from "react-native";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
|
|
||||||
export interface SettingsRowProps {
|
|
||||||
title: string;
|
|
||||||
icon: keyof typeof Ionicons.glyphMap;
|
|
||||||
value?: string;
|
|
||||||
showChevron?: boolean;
|
|
||||||
onPress: () => void;
|
|
||||||
isLast?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ACCENT = "#a855f7"; // single accent (full theming is a separate sub-project)
|
|
||||||
|
|
||||||
export const SettingsRow: React.FC<SettingsRowProps> = ({
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
value,
|
|
||||||
showChevron = true,
|
|
||||||
onPress,
|
|
||||||
isLast = false,
|
|
||||||
}) => {
|
|
||||||
const haptic = useHaptic("light"); // no-op when disableHapticFeedback is set
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
haptic();
|
|
||||||
onPress();
|
|
||||||
}}
|
|
||||||
className={`flex flex-row items-center bg-neutral-900 h-[48px] px-3 ${
|
|
||||||
isLast ? "" : "border-b border-[#ffffff14]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
className='h-[29px] w-[29px] rounded-[7px] items-center justify-center mr-3'
|
|
||||||
style={{ backgroundColor: `${ACCENT}29` }}
|
|
||||||
>
|
|
||||||
<Ionicons name={icon} size={17} color='#c79bff' />
|
|
||||||
</View>
|
|
||||||
<Text className='flex-1 text-white text-[15px]' numberOfLines={1}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{value ? (
|
|
||||||
<Text className='text-[#9899A1] text-[15px] ml-2' numberOfLines={1}>
|
|
||||||
{value}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{showChevron ? (
|
|
||||||
<Ionicons
|
|
||||||
name='chevron-forward'
|
|
||||||
size={17}
|
|
||||||
color='#5A5960'
|
|
||||||
style={{ marginLeft: 4 }}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import type React from "react";
|
|
||||||
import { TextInput, TouchableOpacity, View } from "react-native";
|
|
||||||
|
|
||||||
export const SettingsSearchBar: React.FC<{
|
|
||||||
value: string;
|
|
||||||
onChange: (v: string) => void;
|
|
||||||
}> = ({ value, onChange }) => (
|
|
||||||
<View className='mx-3 mb-4 h-[38px] rounded-xl bg-neutral-800 flex-row items-center px-3'>
|
|
||||||
<Ionicons name='search' size={16} color='#76767c' />
|
|
||||||
<TextInput
|
|
||||||
value={value}
|
|
||||||
onChangeText={onChange}
|
|
||||||
placeholder={t("home.settings.search_placeholder")}
|
|
||||||
placeholderTextColor='#76767c'
|
|
||||||
className='flex-1 ml-2 text-white text-[15px]'
|
|
||||||
autoCapitalize='none'
|
|
||||||
autoCorrect={false}
|
|
||||||
returnKeyType='search'
|
|
||||||
/>
|
|
||||||
{value.length > 0 ? (
|
|
||||||
<TouchableOpacity onPress={() => onChange("")} hitSlop={8}>
|
|
||||||
<Ionicons name='close-circle' size={18} color='#76767c' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
|
|
||||||
export const SettingsSection: React.FC<{
|
|
||||||
title?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}> = ({ title, children }) => (
|
|
||||||
<View className='mb-5'>
|
|
||||||
{title ? (
|
|
||||||
<Text className='ml-4 mb-1.5 uppercase text-[#8E8D91] text-[11px] tracking-wide'>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<View className='mx-3 rounded-xl overflow-hidden bg-neutral-900'>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import type React from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
|
||||||
import {
|
|
||||||
type OptionGroup,
|
|
||||||
PlatformDropdown,
|
|
||||||
} from "@/components/PlatformDropdown";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
valueLabel?: string;
|
|
||||||
groups: OptionGroup[];
|
|
||||||
dropdownTitle?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsSelectRow: React.FC<Props> = ({
|
|
||||||
title,
|
|
||||||
valueLabel,
|
|
||||||
groups,
|
|
||||||
dropdownTitle,
|
|
||||||
disabled,
|
|
||||||
}) => (
|
|
||||||
<ListItem title={title} disabled={disabled}>
|
|
||||||
<PlatformDropdown
|
|
||||||
groups={groups}
|
|
||||||
title={dropdownTitle}
|
|
||||||
trigger={
|
|
||||||
<View className='flex flex-row items-center justify-between pl-3'>
|
|
||||||
<Text className='mr-1 text-[#8E8D91]'>{valueLabel}</Text>
|
|
||||||
<Ionicons name='chevron-expand-sharp' size={18} color='#5A5960' />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { Stepper } from "@/components/inputs/Stepper";
|
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
value: number;
|
|
||||||
step: number;
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
onUpdate: (value: number) => void;
|
|
||||||
appendValue?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsStepperRow: React.FC<Props> = ({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
value,
|
|
||||||
step,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
onUpdate,
|
|
||||||
appendValue,
|
|
||||||
disabled,
|
|
||||||
}) => (
|
|
||||||
<ListItem title={title} subtitle={subtitle} disabled={disabled}>
|
|
||||||
<Stepper
|
|
||||||
value={value}
|
|
||||||
step={step}
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
onUpdate={onUpdate}
|
|
||||||
appendValue={appendValue}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { Switch } from "react-native";
|
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
value: boolean;
|
|
||||||
onValueChange: (value: boolean) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsSwitchRow: React.FC<Props> = ({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
value,
|
|
||||||
onValueChange,
|
|
||||||
disabled,
|
|
||||||
}) => (
|
|
||||||
<ListItem title={title} subtitle={subtitle} disabled={disabled}>
|
|
||||||
<Switch value={value} disabled={disabled} onValueChange={onValueChange} />
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { expect, test } from "bun:test";
|
|
||||||
import { matchesQuery, normalize } from "./searchFilter";
|
|
||||||
|
|
||||||
test("normalize strips accents and lowercases", () => {
|
|
||||||
expect(normalize("Légèreté")).toBe("legerete");
|
|
||||||
expect(normalize(" AUDIO ")).toBe("audio");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("matchesQuery matches title case/accent-insensitively", () => {
|
|
||||||
expect(matchesQuery({ title: "Apparence", keywords: [] }, "appar")).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
matchesQuery({ title: "Audio", keywords: ["sous-titres"] }, "SOUS"),
|
|
||||||
).toBe(true);
|
|
||||||
expect(matchesQuery({ title: "Music", keywords: [] }, "xyz")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("matchesQuery returns true for empty query", () => {
|
|
||||||
expect(matchesQuery({ title: "Anything" }, "")).toBe(true);
|
|
||||||
expect(matchesQuery({ title: "Anything" }, " ")).toBe(true);
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export const normalize = (s: string): string =>
|
|
||||||
s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
||||||
|
|
||||||
export interface Searchable {
|
|
||||||
title: string;
|
|
||||||
keywords?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const matchesQuery = (item: Searchable, query: string): boolean => {
|
|
||||||
const q = normalize(query);
|
|
||||||
if (!q) return true;
|
|
||||||
const hay = normalize([item.title, ...(item.keywords ?? [])].join(" "));
|
|
||||||
return hay.includes(q);
|
|
||||||
};
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import type { Ionicons } from "@expo/vector-icons";
|
|
||||||
|
|
||||||
export type SettingsTarget =
|
|
||||||
| { type: "route"; route: string }
|
|
||||||
| { type: "action"; action: "quickConnect" };
|
|
||||||
|
|
||||||
export interface SettingsEntry {
|
|
||||||
id: string;
|
|
||||||
titleKey: string;
|
|
||||||
icon: keyof typeof Ionicons.glyphMap;
|
|
||||||
target: SettingsTarget;
|
|
||||||
/** extra search terms (English); the title is always searched too */
|
|
||||||
keywords?: string[];
|
|
||||||
/** when set, entry only shows on these platforms */
|
|
||||||
platforms?: ("ios" | "android")[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SettingsSectionDef {
|
|
||||||
id: string;
|
|
||||||
titleKey: string;
|
|
||||||
entries: SettingsEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single source of truth for the Settings index: drives both rendering and search.
|
|
||||||
*
|
|
||||||
* EXTENSIBLE SHELL — to add a setting that lands from an in-flight PR, append one
|
|
||||||
* entry to the right section (and, if it lives inside a sub-page, an entry in
|
|
||||||
* settingsSearchIndex.ts). No screen rewrite needed. Reserved slots (add when the
|
|
||||||
* PR merges): sleep timer #922, sync-play #1612, wake-on-LAN #1539 (advanced);
|
|
||||||
* download location #1486/#1193, clear image cache #1589 (storage/downloads);
|
|
||||||
* double-tap seek #1219/#1289, subtitle background #1543 (playback).
|
|
||||||
*/
|
|
||||||
export const SETTINGS_CATALOG: SettingsSectionDef[] = [
|
|
||||||
{
|
|
||||||
id: "playback",
|
|
||||||
titleKey: "home.settings.categories.playback",
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
id: "playback-controls",
|
|
||||||
titleKey: "home.settings.playback_controls.title",
|
|
||||||
icon: "play",
|
|
||||||
target: { type: "route", route: "/settings/playback-controls/page" },
|
|
||||||
keywords: ["speed", "skip", "autoplay", "orientation"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "audio-subtitles",
|
|
||||||
titleKey: "home.settings.audio_subtitles.title",
|
|
||||||
icon: "chatbox-ellipses",
|
|
||||||
target: { type: "route", route: "/settings/audio-subtitles/page" },
|
|
||||||
keywords: ["subtitle", "audio", "language"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "music",
|
|
||||||
titleKey: "home.settings.music.title",
|
|
||||||
icon: "musical-notes",
|
|
||||||
target: { type: "route", route: "/settings/music/page" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "personalization",
|
|
||||||
titleKey: "home.settings.categories.personalization",
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
id: "appearance",
|
|
||||||
titleKey: "home.settings.appearance.title",
|
|
||||||
icon: "color-palette",
|
|
||||||
target: { type: "route", route: "/settings/appearance/page" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "notifications",
|
|
||||||
titleKey: "home.settings.notifications.title",
|
|
||||||
icon: "notifications",
|
|
||||||
target: { type: "route", route: "/settings/notifications/page" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "advanced",
|
|
||||||
titleKey: "home.settings.categories.advanced",
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
id: "quick-connect",
|
|
||||||
titleKey: "home.settings.quick_connect.quick_connect_title",
|
|
||||||
icon: "key",
|
|
||||||
target: { type: "action", action: "quickConnect" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pair",
|
|
||||||
titleKey: "pairing.pair_with_phone",
|
|
||||||
icon: "phone-portrait",
|
|
||||||
target: {
|
|
||||||
type: "route",
|
|
||||||
route: "/(auth)/(tabs)/(home)/companion-login",
|
|
||||||
},
|
|
||||||
platforms: ["android"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "plugins",
|
|
||||||
titleKey: "home.settings.plugins.plugins_title",
|
|
||||||
icon: "extension-puzzle",
|
|
||||||
target: { type: "route", route: "/settings/plugins/page" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "network",
|
|
||||||
titleKey: "home.settings.network.title",
|
|
||||||
icon: "wifi",
|
|
||||||
target: { type: "route", route: "/settings/network/page" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "logs",
|
|
||||||
titleKey: "home.settings.logs.logs_title",
|
|
||||||
icon: "document-text",
|
|
||||||
target: { type: "route", route: "/settings/logs/page" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "intro",
|
|
||||||
titleKey: "home.settings.intro.title",
|
|
||||||
icon: "information-circle",
|
|
||||||
target: { type: "route", route: "/settings/intro/page" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
export interface SearchableOption {
|
|
||||||
titleKey: string;
|
|
||||||
parentRoute: string;
|
|
||||||
parentTitleKey: string;
|
|
||||||
keywords?: string[];
|
|
||||||
platforms?: ("ios" | "android")[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal options of sub-pages, for deep search ("index + internal settings").
|
|
||||||
* Populated from a per-sub-page audit of every user-facing ListItem/dropdown row.
|
|
||||||
* Every titleKey/parentTitleKey MUST be a real existing i18n key.
|
|
||||||
*/
|
|
||||||
export const SETTINGS_SEARCH_INDEX: SearchableOption[] = [
|
|
||||||
// --- Playback & Controls -------------------------------------------------
|
|
||||||
// MediaToggles
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.media_controls.forward_skip_length",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["skip", "forward", "seconds"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.media_controls.rewind_length",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["rewind", "back", "seconds"],
|
|
||||||
},
|
|
||||||
// GestureControls
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.gesture_controls.horizontal_swipe_skip",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["gesture", "swipe", "skip", "seek"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.gesture_controls.left_side_brightness",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["gesture", "swipe", "brightness", "left"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.gesture_controls.right_side_volume",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["gesture", "swipe", "volume", "right"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.gesture_controls.hide_volume_slider",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["volume", "slider", "hide", "gesture"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.gesture_controls.hide_brightness_slider",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["brightness", "slider", "hide", "gesture"],
|
|
||||||
},
|
|
||||||
// PlaybackControlsSettings (home.settings.other.*)
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.video_orientation",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["orientation", "rotate", "landscape", "portrait"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.safe_area_in_controls",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["safe", "area", "notch", "controls", "inset"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.default_quality",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["quality", "bitrate", "resolution"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.default_playback_speed",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["playback", "speed", "rate"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.disable_haptic_feedback",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["haptic", "vibration", "feedback"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.auto_play_next_episode",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["autoplay", "auto", "next", "episode"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.max_auto_play_episode_count",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["autoplay", "auto", "episode", "count", "limit"],
|
|
||||||
},
|
|
||||||
// MpvBufferSettings
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.buffer.cache_mode",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["cache", "buffer", "mode"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.buffer.buffer_duration",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["buffer", "duration", "cache", "seconds"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.buffer.max_cache_size",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["buffer", "cache", "size", "memory"],
|
|
||||||
},
|
|
||||||
// MpvVoSettings (Android only)
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.vo_driver.vo_mode",
|
|
||||||
parentRoute: "/settings/playback-controls/page",
|
|
||||||
parentTitleKey: "home.settings.playback_controls.title",
|
|
||||||
keywords: ["video", "output", "driver", "gpu", "mpv"],
|
|
||||||
platforms: ["android"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Audio & Subtitles ---------------------------------------------------
|
|
||||||
// AudioToggles
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.audio.set_audio_track",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["audio", "track", "remember", "previous"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.audio.audio_language",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["audio", "language", "default"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.audio.transcode_mode.title",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["audio", "transcode", "surround", "stereo", "passthrough"],
|
|
||||||
},
|
|
||||||
// SubtitleToggles
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.subtitles.subtitle_language",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["subtitle", "caption", "language", "default"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.subtitles.subtitle_mode",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["subtitle", "caption", "mode", "forced"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.subtitles.set_subtitle_track",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["subtitle", "caption", "track", "remember", "previous"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.subtitles.subtitle_size",
|
|
||||||
parentRoute: "/settings/audio-subtitles/page",
|
|
||||||
parentTitleKey: "home.settings.audio_subtitles.title",
|
|
||||||
keywords: ["subtitle", "size", "caption", "scale", "font"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Music ---------------------------------------------------------------
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.music.prefer_downloaded",
|
|
||||||
parentRoute: "/settings/music/page",
|
|
||||||
parentTitleKey: "home.settings.music.title",
|
|
||||||
keywords: ["music", "downloaded", "offline", "local"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.music.lookahead_enabled",
|
|
||||||
parentRoute: "/settings/music/page",
|
|
||||||
parentTitleKey: "home.settings.music.title",
|
|
||||||
keywords: ["music", "lookahead", "cache", "prefetch"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.music.lookahead_count",
|
|
||||||
parentRoute: "/settings/music/page",
|
|
||||||
parentTitleKey: "home.settings.music.title",
|
|
||||||
keywords: ["music", "lookahead", "cache", "count", "tracks"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.music.max_cache_size",
|
|
||||||
parentRoute: "/settings/music/page",
|
|
||||||
parentTitleKey: "home.settings.music.title",
|
|
||||||
keywords: ["music", "cache", "size", "storage"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.storage.clear_music_cache",
|
|
||||||
parentRoute: "/settings/music/page",
|
|
||||||
parentTitleKey: "home.settings.music.title",
|
|
||||||
keywords: ["music", "cache", "clear", "storage"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.storage.delete_all_downloaded_songs",
|
|
||||||
parentRoute: "/settings/music/page",
|
|
||||||
parentTitleKey: "home.settings.music.title",
|
|
||||||
keywords: ["music", "downloaded", "delete", "songs", "storage"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Appearance ----------------------------------------------------------
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.show_custom_menu_links",
|
|
||||||
parentRoute: "/settings/appearance/page",
|
|
||||||
parentTitleKey: "home.settings.appearance.title",
|
|
||||||
keywords: ["menu", "links", "custom", "navigation"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.appearance.merge_next_up_continue_watching",
|
|
||||||
parentRoute: "/settings/appearance/page",
|
|
||||||
parentTitleKey: "home.settings.appearance.title",
|
|
||||||
keywords: ["continue", "watching", "next", "up", "merge", "home"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.other.hide_libraries",
|
|
||||||
parentRoute: "/settings/appearance/page",
|
|
||||||
parentTitleKey: "home.settings.appearance.title",
|
|
||||||
keywords: ["hide", "libraries", "library", "home"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.appearance.hide_remote_session_button",
|
|
||||||
parentRoute: "/settings/appearance/page",
|
|
||||||
parentTitleKey: "home.settings.appearance.title",
|
|
||||||
keywords: ["remote", "session", "button", "hide", "cast"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Network -------------------------------------------------------------
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.network.remote_url",
|
|
||||||
parentRoute: "/settings/network/page",
|
|
||||||
parentTitleKey: "home.settings.network.title",
|
|
||||||
keywords: ["remote", "url", "server", "address"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.network.active_url",
|
|
||||||
parentRoute: "/settings/network/page",
|
|
||||||
parentTitleKey: "home.settings.network.title",
|
|
||||||
keywords: ["active", "url", "server", "connection"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.network.auto_switch_enabled",
|
|
||||||
parentRoute: "/settings/network/page",
|
|
||||||
parentTitleKey: "home.settings.network.title",
|
|
||||||
keywords: ["auto", "switch", "local", "wifi", "network"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: "home.settings.network.local_url",
|
|
||||||
parentRoute: "/settings/network/page",
|
|
||||||
parentTitleKey: "home.settings.network.title",
|
|
||||||
keywords: ["local", "url", "lan", "server", "address"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
import { matchesQuery } from "./searchFilter";
|
|
||||||
import { SETTINGS_CATALOG, type SettingsTarget } from "./settingsCatalog";
|
|
||||||
import { SETTINGS_SEARCH_INDEX } from "./settingsSearchIndex";
|
|
||||||
|
|
||||||
export interface SearchResult {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon: keyof typeof Ionicons.glyphMap;
|
|
||||||
subtitle?: string;
|
|
||||||
target: SettingsTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSettingsSearch = (query: string): SearchResult[] => {
|
|
||||||
const os: "ios" | "android" = Platform.OS === "ios" ? "ios" : "android";
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!query.trim()) return [];
|
|
||||||
const results: SearchResult[] = [];
|
|
||||||
for (const section of SETTINGS_CATALOG) {
|
|
||||||
for (const e of section.entries) {
|
|
||||||
if (e.platforms && !e.platforms.includes(os)) continue;
|
|
||||||
const title = t(e.titleKey);
|
|
||||||
if (matchesQuery({ title, keywords: e.keywords }, query)) {
|
|
||||||
results.push({ id: e.id, title, icon: e.icon, target: e.target });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const o of SETTINGS_SEARCH_INDEX) {
|
|
||||||
if (o.platforms && !o.platforms.includes(os)) continue;
|
|
||||||
const title = t(o.titleKey);
|
|
||||||
if (matchesQuery({ title, keywords: o.keywords }, query)) {
|
|
||||||
results.push({
|
|
||||||
id: `${o.parentRoute}#${o.titleKey}`,
|
|
||||||
title,
|
|
||||||
icon: "search",
|
|
||||||
subtitle: t(o.parentTitleKey),
|
|
||||||
target: { type: "route", route: o.parentRoute },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}, [query, os]);
|
|
||||||
};
|
|
||||||
@@ -213,7 +213,7 @@ public class MpvPlayerModule: Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Defines events that the view can send to JavaScript
|
// Defines events that the view can send to JavaScript
|
||||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class MpvPlayerView: ExpoView {
|
|||||||
let onProgress = EventDispatcher()
|
let onProgress = EventDispatcher()
|
||||||
let onError = EventDispatcher()
|
let onError = EventDispatcher()
|
||||||
let onTracksReady = EventDispatcher()
|
let onTracksReady = EventDispatcher()
|
||||||
|
let onPictureInPictureChange = EventDispatcher()
|
||||||
|
|
||||||
private var currentURL: URL?
|
private var currentURL: URL?
|
||||||
private var cachedPosition: Double = 0
|
private var cachedPosition: Double = 0
|
||||||
@@ -637,6 +638,9 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
print("PiP did start: \(didStartPictureInPicture)")
|
print("PiP did start: \(didStartPictureInPicture)")
|
||||||
// Ensure current time is synced when PiP starts
|
// Ensure current time is synced when PiP starts
|
||||||
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
|
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
|
||||||
|
// Notify JS of the actual PiP active state. `didStartPictureInPicture`
|
||||||
|
// is `false` when AVKit reports a failure to start, so reflect that.
|
||||||
|
onPictureInPictureChange(["isActive": didStartPictureInPicture])
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) {
|
func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) {
|
||||||
@@ -655,6 +659,9 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
if _isZoomedToFill {
|
if _isZoomedToFill {
|
||||||
displayLayer.videoGravity = .resizeAspectFill
|
displayLayer.videoGravity = .resizeAspectFill
|
||||||
}
|
}
|
||||||
|
// Notify JS that PiP has fully stopped so the controls overlay can
|
||||||
|
// be re-mounted when the user returns to full screen.
|
||||||
|
onPictureInPictureChange(["isActive": false])
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
|
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
"expo-brightness": "~56.0.5",
|
"expo-brightness": "~56.0.5",
|
||||||
"expo-build-properties": "~56.0.15",
|
"expo-build-properties": "~56.0.15",
|
||||||
"expo-camera": "~56.0.7",
|
"expo-camera": "~56.0.7",
|
||||||
"expo-clipboard": "~56.0.3",
|
|
||||||
"expo-constants": "~56.0.16",
|
"expo-constants": "~56.0.16",
|
||||||
"expo-crypto": "~56.0.4",
|
"expo-crypto": "~56.0.4",
|
||||||
"expo-dev-client": "~56.0.16",
|
"expo-dev-client": "~56.0.16",
|
||||||
@@ -125,10 +124,8 @@
|
|||||||
"@biomejs/biome": "2.4.16",
|
"@biomejs/biome": "2.4.16",
|
||||||
"@react-native-community/cli": "20.1.3",
|
"@react-native-community/cli": "20.1.3",
|
||||||
"@react-native-tvos/config-tv": "0.1.6",
|
"@react-native-tvos/config-tv": "0.1.6",
|
||||||
"@types/bun": "^1.3.14",
|
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lodash": "4.17.24",
|
"@types/lodash": "4.17.24",
|
||||||
"@types/node": "^24",
|
|
||||||
"@types/react": "~19.2.10",
|
"@types/react": "~19.2.10",
|
||||||
"@types/react-test-renderer": "19.1.0",
|
"@types/react-test-renderer": "19.1.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|||||||
import type * as NotificationsType from "expo-notifications";
|
import type * as NotificationsType from "expo-notifications";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { storage } from "@/utils/mmkv";
|
|
||||||
|
|
||||||
// Conditionally import expo-notifications only on non-TV platforms
|
// Conditionally import expo-notifications only on non-TV platforms
|
||||||
const Notifications = Platform.isTV
|
const Notifications = Platform.isTV
|
||||||
@@ -68,14 +67,6 @@ export async function sendDownloadNotification(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (Platform.isTV || !Notifications) return;
|
if (Platform.isTV || !Notifications) return;
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = storage.getString("settings");
|
|
||||||
const s = raw ? JSON.parse(raw) : {};
|
|
||||||
if (s.notificationsEnabled === false || s.notifyDownloads === false) return;
|
|
||||||
} catch {
|
|
||||||
// ignore parse errors; fall through to sending (defaults are enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Notifications.scheduleNotificationAsync({
|
await Notifications.scheduleNotificationAsync({
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -619,44 +619,54 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setUser(storedUser);
|
setUser(storedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getUserApi(apiInstance).getCurrentUser();
|
// Dismiss splash screen with cached data immediately,
|
||||||
setUser(response.data);
|
// fetch fresh user data in the background
|
||||||
|
setInitialLoaded(true);
|
||||||
|
|
||||||
// Migrate current session to secure storage if not already saved
|
try {
|
||||||
if (storedUser?.Id && storedUser?.Name) {
|
const response = await getUserApi(apiInstance).getCurrentUser();
|
||||||
const existingCredential = await getAccountCredential(
|
setUser(response.data);
|
||||||
serverUrl,
|
|
||||||
storedUser.Id,
|
// Migrate current session to secure storage if not already saved
|
||||||
);
|
if (storedUser?.Id && storedUser?.Name) {
|
||||||
if (!existingCredential) {
|
const existingCredential = await getAccountCredential(
|
||||||
await saveAccountCredential({
|
|
||||||
serverUrl,
|
serverUrl,
|
||||||
serverName: "",
|
storedUser.Id,
|
||||||
token,
|
);
|
||||||
userId: storedUser.Id,
|
if (!existingCredential) {
|
||||||
username: storedUser.Name,
|
await saveAccountCredential({
|
||||||
savedAt: Date.now(),
|
serverUrl,
|
||||||
securityType: "none",
|
serverName: "",
|
||||||
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
token,
|
||||||
});
|
userId: storedUser.Id,
|
||||||
} else if (
|
username: storedUser.Name,
|
||||||
response.data.PrimaryImageTag !==
|
savedAt: Date.now(),
|
||||||
existingCredential.primaryImageTag
|
securityType: "none",
|
||||||
) {
|
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
||||||
// Update image tag if it has changed
|
});
|
||||||
addAccountToServer(serverUrl, existingCredential.serverName, {
|
} else if (
|
||||||
userId: existingCredential.userId,
|
response.data.PrimaryImageTag !==
|
||||||
username: existingCredential.username,
|
existingCredential.primaryImageTag
|
||||||
securityType: existingCredential.securityType,
|
) {
|
||||||
savedAt: existingCredential.savedAt,
|
// Update image tag if it has changed
|
||||||
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
addAccountToServer(serverUrl, existingCredential.serverName, {
|
||||||
});
|
userId: existingCredential.userId,
|
||||||
|
username: existingCredential.username,
|
||||||
|
securityType: existingCredential.securityType,
|
||||||
|
savedAt: existingCredential.savedAt,
|
||||||
|
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Background fetch failed — app already rendered with cached data
|
||||||
|
console.warn("Background user fetch failed, using cached data:", e);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setInitialLoaded(true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
|
||||||
setInitialLoaded(true);
|
setInitialLoaded(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
236
scripts/detect-duplicate-issue.mjs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Flags likely-duplicate issues when a new issue is opened, using lexical similarity
|
||||||
|
* (Jaccard over word sets of the title and body) — no API key, no embeddings.
|
||||||
|
*
|
||||||
|
* On a match it posts ONE comment listing the closest open issues and adds the
|
||||||
|
* "possible duplicate" label. If nothing is similar enough, it does nothing.
|
||||||
|
*
|
||||||
|
* Env:
|
||||||
|
* GITHUB_REPOSITORY owner/repo
|
||||||
|
* ISSUE_NUMBER the new issue number
|
||||||
|
* ISSUE_TITLE the new issue title
|
||||||
|
* ISSUE_BODY the new issue body
|
||||||
|
* GH_TOKEN/GITHUB_TOKEN for gh (provided in CI)
|
||||||
|
* DUP_THRESHOLD similarity threshold 0..1 (default 0.3)
|
||||||
|
* DUP_MAX max matches to report (default 5)
|
||||||
|
* DUP_FIXTURE optional path to a JSON array of {number,title,body} (local testing)
|
||||||
|
* DRY_RUN if set, print results instead of commenting/labelling
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
// Parse a numeric env var, falling back to `def` only when unset/empty/NaN so an explicit 0 is honoured.
|
||||||
|
const numEnv = (name, def) => {
|
||||||
|
const raw = process.env[name];
|
||||||
|
if (raw === undefined || raw === "") return def;
|
||||||
|
const n = Number(raw);
|
||||||
|
return Number.isNaN(n) ? def : n;
|
||||||
|
};
|
||||||
|
|
||||||
|
const REPO = process.env.GITHUB_REPOSITORY || "streamyfin/streamyfin";
|
||||||
|
const NUMBER = numEnv("ISSUE_NUMBER", Number.NaN);
|
||||||
|
const TITLE = process.env.ISSUE_TITLE || "";
|
||||||
|
const BODY = process.env.ISSUE_BODY || "";
|
||||||
|
const THRESHOLD = numEnv("DUP_THRESHOLD", 0.3);
|
||||||
|
const MAX = numEnv("DUP_MAX", 5);
|
||||||
|
const DRY = !!process.env.DRY_RUN;
|
||||||
|
const LABEL = "possible duplicate";
|
||||||
|
const MARKER = "<!-- duplicate-detector -->";
|
||||||
|
|
||||||
|
// Generic stop words only — keep domain/feature/platform words (android, downloads,
|
||||||
|
// subtitles…) since those are exactly what makes two reports the same or different.
|
||||||
|
const STOP = new Set(
|
||||||
|
(
|
||||||
|
"a an the and or but if then of to in on at by for with from as is are was were be been being do does did " +
|
||||||
|
"it its this that these those i you we they me my your our their he she him her " +
|
||||||
|
"when while where what which who how why so just then than too very can could would should will " +
|
||||||
|
"not no nor only own same s t don dont im ive please thanks hi hello also still get got use used using " +
|
||||||
|
"app application streamyfin issue bug"
|
||||||
|
).split(/\s+/),
|
||||||
|
);
|
||||||
|
|
||||||
|
const stem = (w) => w.replace(/(ing|ed|es|s)$/, "");
|
||||||
|
|
||||||
|
const tokens = (s) =>
|
||||||
|
(s || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/```[\s\S]*?```/g, " ") // drop code blocks
|
||||||
|
.replace(/<!--[\s\S]*?-->/g, " ") // drop html comments
|
||||||
|
.replace(/https?:\/\/\S+/g, " ") // drop urls
|
||||||
|
.replace(/[^a-z0-9\s]/g, " ")
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((w) => w.length > 2 && !STOP.has(w))
|
||||||
|
.map(stem)
|
||||||
|
.filter((w) => w.length > 2);
|
||||||
|
|
||||||
|
const jaccard = (a, b) => {
|
||||||
|
const A = new Set(a);
|
||||||
|
const B = new Set(b);
|
||||||
|
if (!A.size || !B.size) return 0;
|
||||||
|
let inter = 0;
|
||||||
|
for (const x of A) if (B.has(x)) inter++;
|
||||||
|
return inter / (A.size + B.size - inter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const newTitle = tokens(TITLE);
|
||||||
|
const newBody = tokens(BODY);
|
||||||
|
const score = (o) =>
|
||||||
|
0.6 * jaccard(newTitle, tokens(o.title)) +
|
||||||
|
0.4 * jaccard(newBody, tokens(o.body));
|
||||||
|
|
||||||
|
// fetch open issues (excluding PRs and the new issue itself)
|
||||||
|
let issues;
|
||||||
|
if (process.env.DUP_FIXTURE) {
|
||||||
|
issues = JSON.parse(readFileSync(process.env.DUP_FIXTURE, "utf8"));
|
||||||
|
} else {
|
||||||
|
const raw = execFileSync(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"api",
|
||||||
|
`repos/${REPO}/issues`,
|
||||||
|
"--paginate",
|
||||||
|
"-X",
|
||||||
|
"GET",
|
||||||
|
"-f",
|
||||||
|
"state=open",
|
||||||
|
"-f",
|
||||||
|
"per_page=100",
|
||||||
|
"--jq",
|
||||||
|
".[] | select(.pull_request | not) | {number, title, body}",
|
||||||
|
],
|
||||||
|
{ encoding: "utf8", maxBuffer: 1e8 },
|
||||||
|
);
|
||||||
|
issues = raw
|
||||||
|
.split("\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((l) => JSON.parse(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = issues
|
||||||
|
.filter((o) => o.number !== NUMBER)
|
||||||
|
.map((o) => ({ ...o, s: score(o) }))
|
||||||
|
.filter((o) => o.s >= THRESHOLD)
|
||||||
|
.sort((a, b) => b.s - a.s)
|
||||||
|
.slice(0, MAX);
|
||||||
|
|
||||||
|
if (!matches.length) {
|
||||||
|
console.log("No likely duplicates found.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neutralise other issues' titles before echoing them back: break @mentions and
|
||||||
|
// strip markdown/HTML control chars so a maliciously-named issue can't ping people
|
||||||
|
// or inject formatting into our comment. GitHub linkifies "#123" on its own.
|
||||||
|
const safeTitle = (t) =>
|
||||||
|
(t || "")
|
||||||
|
.replace(/@/g, "@")
|
||||||
|
.replace(/[`<>|*_~[\]]/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 140);
|
||||||
|
const list = matches
|
||||||
|
.map(
|
||||||
|
(m) =>
|
||||||
|
`- #${m.number} — ${safeTitle(m.title)} (≈ ${Math.round(m.s * 100)}% similar)`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
const comment = [
|
||||||
|
MARKER,
|
||||||
|
"🔍 **This looks like it might be a duplicate.** Possibly related open issues:",
|
||||||
|
"",
|
||||||
|
list,
|
||||||
|
"",
|
||||||
|
"If yours is different, ignore this — a maintainer will confirm. Otherwise, please 👍 the existing issue and add any extra details there.",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
console.log(`Found ${matches.length} possible duplicate(s):\n${list}`);
|
||||||
|
|
||||||
|
if (DRY) {
|
||||||
|
console.log("\nDRY_RUN: not commenting/labelling.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live mode needs a real issue number; refuse rather than POST to /issues/NaN/...
|
||||||
|
if (!Number.isInteger(NUMBER) || NUMBER <= 0) {
|
||||||
|
console.error(
|
||||||
|
`Invalid ISSUE_NUMBER ${JSON.stringify(process.env.ISSUE_NUMBER)} — refusing to comment.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotency: skip if we've already flagged this issue (guards re-runs / future triggers).
|
||||||
|
const priorComments = execFileSync(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"api",
|
||||||
|
`repos/${REPO}/issues/${NUMBER}/comments`,
|
||||||
|
"--paginate",
|
||||||
|
"--jq",
|
||||||
|
".[].body",
|
||||||
|
],
|
||||||
|
{ encoding: "utf8", maxBuffer: 1e8 },
|
||||||
|
);
|
||||||
|
if (priorComments.includes(MARKER)) {
|
||||||
|
console.log("Already flagged (marker present); skipping.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
execFileSync(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"api",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
`repos/${REPO}/issues/${NUMBER}/comments`,
|
||||||
|
"-f",
|
||||||
|
`body=${comment}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
execFileSync(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"api",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
`repos/${REPO}/issues/${NUMBER}/labels`,
|
||||||
|
"-f",
|
||||||
|
`labels[]=${LABEL}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// label may not exist yet — create then add
|
||||||
|
execFileSync(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"api",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
`repos/${REPO}/labels`,
|
||||||
|
"-f",
|
||||||
|
`name=${LABEL}`,
|
||||||
|
"-f",
|
||||||
|
"color=fbca04",
|
||||||
|
"-f",
|
||||||
|
"description=Automatically flagged as a possible duplicate",
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
execFileSync(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"api",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
`repos/${REPO}/issues/${NUMBER}/labels`,
|
||||||
|
"-f",
|
||||||
|
`labels[]=${LABEL}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("Commented and labelled.");
|
||||||
@@ -119,9 +119,6 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Settings",
|
"settings_title": "Settings",
|
||||||
"log_out_button": "Log Out",
|
"log_out_button": "Log Out",
|
||||||
"search_placeholder": "Search settings",
|
|
||||||
"search_results": "Results",
|
|
||||||
"search_no_results": "No matching settings",
|
|
||||||
"switch_user": {
|
"switch_user": {
|
||||||
"title": "Switch User",
|
"title": "Switch User",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
@@ -129,25 +126,7 @@
|
|||||||
"current": "current"
|
"current": "current"
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories",
|
"title": "Categories"
|
||||||
"playback": "Playback",
|
|
||||||
"personalization": "Personalization",
|
|
||||||
"advanced": "Advanced"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"title": "Notifications",
|
|
||||||
"disabled_title": "Notifications are off",
|
|
||||||
"disabled_description": "Allow notifications to get alerts about your downloads and more.",
|
|
||||||
"enable_button": "Enable notifications",
|
|
||||||
"events_title": "Notify me about",
|
|
||||||
"master": "Enable notifications",
|
|
||||||
"downloads": "Download completed / failed"
|
|
||||||
},
|
|
||||||
"account": {
|
|
||||||
"title": "Account",
|
|
||||||
"copy_token": "Copy token",
|
|
||||||
"copied": "Copied to clipboard",
|
|
||||||
"copy_failed": "Couldn't copy to clipboard"
|
|
||||||
},
|
},
|
||||||
"playback_controls": {
|
"playback_controls": {
|
||||||
"title": "Playback & Controls"
|
"title": "Playback & Controls"
|
||||||
@@ -220,10 +199,6 @@
|
|||||||
"rewind_length": "Rewind Length",
|
"rewind_length": "Rewind Length",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
"chromecast": {
|
|
||||||
"title": "Chromecast",
|
|
||||||
"enable_h265": "Enable H265 for Chromecast"
|
|
||||||
},
|
|
||||||
"buffer": {
|
"buffer": {
|
||||||
"title": "Buffer Settings",
|
"title": "Buffer Settings",
|
||||||
"cache_mode": "Cache Mode",
|
"cache_mode": "Cache Mode",
|
||||||
|
|||||||
@@ -294,12 +294,6 @@ export type Settings = {
|
|||||||
openSubtitlesApiKey?: string;
|
openSubtitlesApiKey?: string;
|
||||||
// TV-only: Inactivity timeout for auto-logout
|
// TV-only: Inactivity timeout for auto-logout
|
||||||
inactivityTimeout: InactivityTimeout;
|
inactivityTimeout: InactivityTimeout;
|
||||||
// Settings-redo additions (SP1)
|
|
||||||
notificationsEnabled: boolean;
|
|
||||||
notifyDownloads: boolean;
|
|
||||||
defaultLandingTab: "(home)" | "(search)" | "(favorites)" | "(libraries)";
|
|
||||||
downloadOnWifiOnly: boolean;
|
|
||||||
cellularBitrate?: Bitrate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Lockable<T> {
|
export interface Lockable<T> {
|
||||||
@@ -401,11 +395,6 @@ export const defaultValues: Settings = {
|
|||||||
audioTranscodeMode: AudioTranscodeMode.Auto,
|
audioTranscodeMode: AudioTranscodeMode.Auto,
|
||||||
// TV-only: Inactivity timeout (disabled by default)
|
// TV-only: Inactivity timeout (disabled by default)
|
||||||
inactivityTimeout: InactivityTimeout.Disabled,
|
inactivityTimeout: InactivityTimeout.Disabled,
|
||||||
notificationsEnabled: true,
|
|
||||||
notifyDownloads: true,
|
|
||||||
defaultLandingTab: "(home)",
|
|
||||||
downloadOnWifiOnly: false,
|
|
||||||
cellularBitrate: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSettings = (): Partial<Settings> => {
|
const loadSettings = (): Partial<Settings> => {
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Platform } from "react-native";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TV-safe re-exports of `@expo/ui/community/bottom-sheet`.
|
|
||||||
*
|
|
||||||
* `@expo/ui` resolves its native bridge at module load via
|
|
||||||
* `requireNativeModule('ExpoUI')`, which does not exist on tvOS — a static
|
|
||||||
* top-level import would crash the whole expo-router route tree. We `require()`
|
|
||||||
* it lazily and only off-TV; on TV the exports are undefined, which is fine
|
|
||||||
* because every call site early-returns on `Platform.isTV`.
|
|
||||||
*/
|
|
||||||
type BottomSheetMod = typeof import("@expo/ui/community/bottom-sheet");
|
|
||||||
|
|
||||||
const mod: BottomSheetMod = Platform.isTV
|
|
||||||
? ({} as BottomSheetMod)
|
|
||||||
: (require("@expo/ui/community/bottom-sheet") as BottomSheetMod);
|
|
||||||
|
|
||||||
export const BottomSheetModal = mod.BottomSheetModal;
|
|
||||||
export const BottomSheetView = mod.BottomSheetView;
|
|
||||||
export const BottomSheetScrollView = mod.BottomSheetScrollView;
|
|
||||||
export const BottomSheetTextInput = mod.BottomSheetTextInput;
|
|
||||||
|
|
||||||
export type { BottomSheetMethods } from "@expo/ui/community/bottom-sheet";
|
|
||||||