mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-12 04:16:37 +00:00
Compare commits
9 Commits
v0.47.1
...
feat/refre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4696671bf5 | ||
|
|
85e5c25206 | ||
|
|
3dc84818e8 | ||
|
|
18102a3045 | ||
|
|
2be78a232c | ||
|
|
30dc3980e3 | ||
|
|
69d744c86f | ||
|
|
ad1bd72123 | ||
|
|
b93c56f300 |
6
.github/workflows/ci-codeql.yml
vendored
6
.github/workflows/ci-codeql.yml
vendored
@@ -31,13 +31,13 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 🏁 Initialize CodeQL
|
- name: 🏁 Initialize CodeQL
|
||||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended,security-and-quality
|
queries: +security-extended,security-and-quality
|
||||||
|
|
||||||
- name: 🛠️ Autobuild
|
- name: 🛠️ Autobuild
|
||||||
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
|
|
||||||
- name: 🧪 Perform CodeQL Analysis
|
- name: 🧪 Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
|
|||||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
|
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||||
with:
|
with:
|
||||||
fail-on-severity: high
|
fail-on-severity: high
|
||||||
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}
|
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}
|
||||||
|
|||||||
@@ -113,33 +113,144 @@ export default function IndexLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='settings/marlin-search/page'
|
name='settings/playback-controls/page'
|
||||||
options={{
|
options={{
|
||||||
title: "",
|
title: t("home.settings.playback_controls.title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
<TouchableOpacity onPress={() => _router.back()} className='pl-0.5'>
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
<Feather name='chevron-left' size={28} color='white' />
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='settings/jellyseerr/page'
|
name='settings/audio-subtitles/page'
|
||||||
options={{
|
options={{
|
||||||
title: "",
|
title: t("home.settings.audio_subtitles.title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
<TouchableOpacity onPress={() => _router.back()} className='pl-0.5'>
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
<Feather name='chevron-left' size={28} color='white' />
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='settings/hide-libraries/page'
|
name='settings/appearance/page'
|
||||||
options={{
|
options={{
|
||||||
title: "",
|
title: t("home.settings.appearance.title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
<TouchableOpacity onPress={() => _router.back()} className='pl-0.5'>
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='settings/appearance/hide-libraries/page'
|
||||||
|
options={{
|
||||||
|
title: t("home.settings.other.hide_libraries"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerLeft: () => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='settings/plugins/page'
|
||||||
|
options={{
|
||||||
|
title: t("home.settings.plugins.plugins_title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerLeft: () => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='settings/plugins/marlin-search/page'
|
||||||
|
options={{
|
||||||
|
title: "Marlin Search",
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerLeft: () => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='settings/plugins/jellyseerr/page'
|
||||||
|
options={{
|
||||||
|
title: "Jellyseerr",
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerLeft: () => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='settings/intro/page'
|
||||||
|
options={{
|
||||||
|
title: t("home.settings.intro.title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerLeft: () => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
<Feather name='chevron-left' size={28} color='white' />
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
@@ -148,9 +259,16 @@ export default function IndexLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='settings/logs/page'
|
name='settings/logs/page'
|
||||||
options={{
|
options={{
|
||||||
title: "",
|
title: t("home.settings.logs.logs_title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
<TouchableOpacity onPress={() => _router.back()} className='pl-0.5'>
|
<TouchableOpacity
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
<Feather name='chevron-left' size={28} color='white' />
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,34 +8,16 @@ import { Text } from "@/components/common/Text";
|
|||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
||||||
import { AudioToggles } from "@/components/settings/AudioToggles";
|
|
||||||
import { ChromecastSettings } from "@/components/settings/ChromecastSettings";
|
|
||||||
import DownloadSettings from "@/components/settings/DownloadSettings";
|
|
||||||
import { GestureControls } from "@/components/settings/GestureControls";
|
|
||||||
import { MediaProvider } from "@/components/settings/MediaContext";
|
|
||||||
import { MediaToggles } from "@/components/settings/MediaToggles";
|
|
||||||
import { OtherSettings } from "@/components/settings/OtherSettings";
|
|
||||||
import { PluginSettings } from "@/components/settings/PluginSettings";
|
|
||||||
import { QuickConnect } from "@/components/settings/QuickConnect";
|
import { QuickConnect } from "@/components/settings/QuickConnect";
|
||||||
import { StorageSettings } from "@/components/settings/StorageSettings";
|
import { StorageSettings } from "@/components/settings/StorageSettings";
|
||||||
import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
|
|
||||||
import { UserInfo } from "@/components/settings/UserInfo";
|
import { UserInfo } from "@/components/settings/UserInfo";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { clearLogs } from "@/utils/log";
|
|
||||||
import { storage } from "@/utils/mmkv";
|
|
||||||
|
|
||||||
export default function settings() {
|
export default function settings() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [_user] = useAtom(userAtom);
|
const [_user] = useAtom(userAtom);
|
||||||
const { logout } = useJellyfin();
|
const { logout } = useJellyfin();
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
|
|
||||||
const onClearLogsClicked = async () => {
|
|
||||||
clearLogs();
|
|
||||||
successHapticFeedback();
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,58 +45,51 @@ export default function settings() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className='p-4 flex flex-col gap-y-4'
|
className='p-4 flex flex-col'
|
||||||
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
>
|
>
|
||||||
<UserInfo />
|
<View className='mb-4'>
|
||||||
|
<UserInfo />
|
||||||
|
</View>
|
||||||
|
|
||||||
<QuickConnect className='mb-4' />
|
<QuickConnect className='mb-4' />
|
||||||
|
|
||||||
<MediaProvider>
|
<View className='mb-4'>
|
||||||
<MediaToggles className='mb-4' />
|
<AppLanguageSelector />
|
||||||
<GestureControls className='mb-4' />
|
</View>
|
||||||
<AudioToggles className='mb-4' />
|
|
||||||
<SubtitleToggles className='mb-4' />
|
|
||||||
</MediaProvider>
|
|
||||||
|
|
||||||
<OtherSettings />
|
|
||||||
|
|
||||||
{!Platform.isTV && <DownloadSettings />}
|
|
||||||
|
|
||||||
<PluginSettings />
|
|
||||||
|
|
||||||
<AppLanguageSelector />
|
|
||||||
|
|
||||||
{!Platform.isTV && <ChromecastSettings />}
|
|
||||||
|
|
||||||
<ListGroup title={"Intro"}>
|
|
||||||
<ListItem
|
|
||||||
onPress={() => {
|
|
||||||
router.push("/intro/page");
|
|
||||||
}}
|
|
||||||
title={t("home.settings.intro.show_intro")}
|
|
||||||
/>
|
|
||||||
<ListItem
|
|
||||||
textColor='red'
|
|
||||||
onPress={() => {
|
|
||||||
storage.set("hasShownIntro", false);
|
|
||||||
}}
|
|
||||||
title={t("home.settings.intro.reset_intro")}
|
|
||||||
/>
|
|
||||||
</ListGroup>
|
|
||||||
|
|
||||||
<View className='mb-4'>
|
<View className='mb-4'>
|
||||||
<ListGroup title={t("home.settings.logs.logs_title")}>
|
<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/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
|
<ListItem
|
||||||
onPress={() => router.push("/settings/logs/page")}
|
onPress={() => router.push("/settings/logs/page")}
|
||||||
showArrow
|
showArrow
|
||||||
title={t("home.settings.logs.logs_title")}
|
title={t("home.settings.logs.logs_title")}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
|
||||||
textColor='red'
|
|
||||||
onPress={onClearLogsClicked}
|
|
||||||
title={t("home.settings.logs.delete_all_logs")}
|
|
||||||
/>
|
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ScrollView, Switch, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { Loader } from "@/components/Loader";
|
||||||
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
|
export default function page() {
|
||||||
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
const user = useAtomValue(userAtom);
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["user-views", user?.Id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await getUserViewsApi(api!).getUserViews({
|
||||||
|
userId: user?.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.Items || null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<View className='mt-4'>
|
||||||
|
<Loader />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DisabledSetting
|
||||||
|
disabled={pluginSettings?.hiddenLibraries?.locked === true}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<ListGroup title={t("home.settings.other.hide_libraries")}>
|
||||||
|
{data?.map((view) => (
|
||||||
|
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
|
||||||
|
<Switch
|
||||||
|
value={settings.hiddenLibraries?.includes(view.Id!) || false}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
updateSettings({
|
||||||
|
hiddenLibraries: value
|
||||||
|
? [...(settings.hiddenLibraries || []), view.Id!]
|
||||||
|
: settings.hiddenLibraries?.filter(
|
||||||
|
(id) => id !== view.Id,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</ListGroup>
|
||||||
|
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||||
|
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
||||||
|
</Text>
|
||||||
|
</DisabledSetting>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
app/(auth)/(tabs)/(home)/settings/appearance/page.tsx
Normal file
25
app/(auth)/(tabs)/(home)/settings/appearance/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { AppearanceSettings } from "@/components/settings/AppearanceSettings";
|
||||||
|
|
||||||
|
export default function AppearancePage() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className='p-4 flex flex-col'
|
||||||
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
|
>
|
||||||
|
<AppearanceSettings />
|
||||||
|
<View className='h-24' />
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx
Normal file
29
app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { AudioToggles } from "@/components/settings/AudioToggles";
|
||||||
|
import { MediaProvider } from "@/components/settings/MediaContext";
|
||||||
|
import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
|
||||||
|
|
||||||
|
export default function AudioSubtitlesPage() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className='p-4 flex flex-col'
|
||||||
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
|
>
|
||||||
|
<MediaProvider>
|
||||||
|
<AudioToggles className='mb-4' />
|
||||||
|
<SubtitleToggles className='mb-4' />
|
||||||
|
</MediaProvider>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
app/(auth)/(tabs)/(home)/settings/intro/page.tsx
Normal file
45
app/(auth)/(tabs)/(home)/settings/intro/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
|
export default function IntroPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className='p-4 flex flex-col'
|
||||||
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
|
>
|
||||||
|
<ListGroup title={t("home.settings.intro.title")}>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/intro/page");
|
||||||
|
}}
|
||||||
|
title={t("home.settings.intro.show_intro")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
textColor='red'
|
||||||
|
onPress={() => {
|
||||||
|
storage.set("hasShownIntro", false);
|
||||||
|
}}
|
||||||
|
title={t("home.settings.intro.reset_intro")}
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
<View className='h-24' />
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
|
||||||
import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const { pluginSettings } = useSettings();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DisabledSetting
|
|
||||||
disabled={pluginSettings?.jellyseerrServerUrl?.locked === true}
|
|
||||||
className='p-4'
|
|
||||||
>
|
|
||||||
<JellyseerrSettings />
|
|
||||||
</DisabledSetting>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { File, Paths } from "expo-file-system";
|
||||||
import { useNavigation } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import * as Sharing from "expo-sharing";
|
import * as Sharing from "expo-sharing";
|
||||||
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
import { ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
import Collapsible from "react-native-collapsible";
|
import Collapsible from "react-native-collapsible";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { FilterButton } from "@/components/filters/FilterButton";
|
import { FilterButton } from "@/components/filters/FilterButton";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
@@ -32,6 +34,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const _orderId = useId();
|
const _orderId = useId();
|
||||||
const _levelsId = useId();
|
const _levelsId = useId();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
const filteredLogs = useMemo(
|
const filteredLogs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -46,18 +49,17 @@ export default function Page() {
|
|||||||
|
|
||||||
// Sharing it as txt while its formatted allows us to share it with many more applications
|
// Sharing it as txt while its formatted allows us to share it with many more applications
|
||||||
const share = useCallback(async () => {
|
const share = useCallback(async () => {
|
||||||
const uri = `${FileSystem.documentDirectory}logs.txt`;
|
const logsFile = new File(Paths.document, "logs.txt");
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
FileSystem.writeAsStringAsync(uri, JSON.stringify(filteredLogs))
|
try {
|
||||||
.then(() => {
|
logsFile.write(JSON.stringify(filteredLogs));
|
||||||
setLoading(false);
|
await Sharing.shareAsync(logsFile.uri, { mimeType: "txt", UTI: "txt" });
|
||||||
Sharing.shareAsync(uri, { mimeType: "txt", UTI: "txt" });
|
} catch (e: any) {
|
||||||
})
|
writeErrorLog("Something went wrong attempting to export", e);
|
||||||
.catch((e) =>
|
} finally {
|
||||||
writeErrorLog("Something went wrong attempting to export", e),
|
setLoading(false);
|
||||||
)
|
}
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, [filteredLogs]);
|
}, [filteredLogs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -66,7 +68,7 @@ export default function Page() {
|
|||||||
loading ? (
|
loading ? (
|
||||||
<Loader />
|
<Loader />
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity onPress={share}>
|
<TouchableOpacity onPress={share} className='px-2'>
|
||||||
<Text>{t("home.settings.logs.export_logs")}</Text>
|
<Text>{t("home.settings.logs.export_logs")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
@@ -74,7 +76,12 @@ export default function Page() {
|
|||||||
}, [share, loading]);
|
}, [share, loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View
|
||||||
|
className='flex-1'
|
||||||
|
style={{
|
||||||
|
paddingTop: insets.top + 48,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<View className='flex flex-row justify-end py-2 px-4 space-x-2'>
|
<View className='flex flex-row justify-end py-2 px-4 space-x-2'>
|
||||||
<FilterButton
|
<FilterButton
|
||||||
id={orderFilterId}
|
id={orderFilterId}
|
||||||
@@ -156,6 +163,6 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useNavigation } from "expo-router";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
Linking,
|
|
||||||
Switch,
|
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { toast } from "sonner-native";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { settings, updateSettings, pluginSettings } = useSettings();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
|
|
||||||
|
|
||||||
const onSave = (val: string) => {
|
|
||||||
updateSettings({
|
|
||||||
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
|
|
||||||
});
|
|
||||||
toast.success(t("home.settings.plugins.marlin_search.toasts.saved"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenLink = () => {
|
|
||||||
Linking.openURL("https://github.com/fredrikburmester/marlin-search");
|
|
||||||
};
|
|
||||||
|
|
||||||
const disabled = useMemo(() => {
|
|
||||||
return (
|
|
||||||
pluginSettings?.searchEngine?.locked === true &&
|
|
||||||
pluginSettings?.marlinServerUrl?.locked === true
|
|
||||||
);
|
|
||||||
}, [pluginSettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pluginSettings?.marlinServerUrl?.locked) {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerRight: () => (
|
|
||||||
<TouchableOpacity onPress={() => onSave(value)}>
|
|
||||||
<Text className='text-blue-500'>
|
|
||||||
{t("home.settings.plugins.marlin_search.save_button")}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [navigation, value]);
|
|
||||||
|
|
||||||
if (!settings) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DisabledSetting disabled={disabled} className='px-4'>
|
|
||||||
<ListGroup>
|
|
||||||
<DisabledSetting
|
|
||||||
disabled={pluginSettings?.searchEngine?.locked === true}
|
|
||||||
showText={!pluginSettings?.marlinServerUrl?.locked}
|
|
||||||
>
|
|
||||||
<ListItem
|
|
||||||
title={t(
|
|
||||||
"home.settings.plugins.marlin_search.enable_marlin_search",
|
|
||||||
)}
|
|
||||||
onPress={() => {
|
|
||||||
updateSettings({ searchEngine: "Jellyfin" });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
value={settings.searchEngine === "Marlin"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</DisabledSetting>
|
|
||||||
</ListGroup>
|
|
||||||
|
|
||||||
<DisabledSetting
|
|
||||||
disabled={pluginSettings?.marlinServerUrl?.locked === true}
|
|
||||||
showText={!pluginSettings?.searchEngine?.locked}
|
|
||||||
className='mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'
|
|
||||||
>
|
|
||||||
<View className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}>
|
|
||||||
<Text className='mr-4'>
|
|
||||||
{t("home.settings.plugins.marlin_search.url")}
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
editable={settings.searchEngine === "Marlin"}
|
|
||||||
className='text-white'
|
|
||||||
placeholder={t(
|
|
||||||
"home.settings.plugins.marlin_search.server_url_placeholder",
|
|
||||||
)}
|
|
||||||
value={value}
|
|
||||||
keyboardType='url'
|
|
||||||
returnKeyType='done'
|
|
||||||
autoCapitalize='none'
|
|
||||||
textContentType='URL'
|
|
||||||
onChangeText={(text) => setValue(text)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</DisabledSetting>
|
|
||||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
|
||||||
{t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "}
|
|
||||||
<Text className='text-blue-500' onPress={handleOpenLink}>
|
|
||||||
{t("home.settings.plugins.marlin_search.read_more_about_marlin")}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</DisabledSetting>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
35
app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx
Normal file
35
app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { GestureControls } from "@/components/settings/GestureControls";
|
||||||
|
import { MediaProvider } from "@/components/settings/MediaContext";
|
||||||
|
import { MediaToggles } from "@/components/settings/MediaToggles";
|
||||||
|
import { PlaybackControlsSettings } from "@/components/settings/PlaybackControlsSettings";
|
||||||
|
import { ChromecastSettings } from "../../../../../../components/settings/ChromecastSettings";
|
||||||
|
|
||||||
|
export default function PlaybackControlsPage() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className='p-4 flex flex-col'
|
||||||
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
|
>
|
||||||
|
<View className='mb-4'>
|
||||||
|
<MediaProvider>
|
||||||
|
<MediaToggles className='mb-4' />
|
||||||
|
<GestureControls className='mb-4' />
|
||||||
|
<PlaybackControlsSettings />
|
||||||
|
</MediaProvider>
|
||||||
|
</View>
|
||||||
|
{!Platform.isTV && <ChromecastSettings />}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ScrollView } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
|
export default function page() {
|
||||||
|
const { pluginSettings } = useSettings();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DisabledSetting
|
||||||
|
disabled={pluginSettings?.jellyseerrServerUrl?.locked === true}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<JellyseerrSettings />
|
||||||
|
</DisabledSetting>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx
Normal file
138
app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useNavigation } from "expo-router";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Linking,
|
||||||
|
ScrollView,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
|
export default function page() {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
|
||||||
|
|
||||||
|
const onSave = (val: string) => {
|
||||||
|
updateSettings({
|
||||||
|
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
|
||||||
|
});
|
||||||
|
toast.success(t("home.settings.plugins.marlin_search.toasts.saved"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenLink = () => {
|
||||||
|
Linking.openURL("https://github.com/fredrikburmester/marlin-search");
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabled = useMemo(() => {
|
||||||
|
return (
|
||||||
|
pluginSettings?.searchEngine?.locked === true &&
|
||||||
|
pluginSettings?.marlinServerUrl?.locked === true
|
||||||
|
);
|
||||||
|
}, [pluginSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pluginSettings?.marlinServerUrl?.locked) {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerRight: () => (
|
||||||
|
<TouchableOpacity onPress={() => onSave(value)} className='px-2'>
|
||||||
|
<Text className='text-blue-500'>
|
||||||
|
{t("home.settings.plugins.marlin_search.save_button")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [navigation, value]);
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DisabledSetting disabled={disabled} className='px-4'>
|
||||||
|
<ListGroup>
|
||||||
|
<DisabledSetting
|
||||||
|
disabled={pluginSettings?.searchEngine?.locked === true}
|
||||||
|
showText={!pluginSettings?.marlinServerUrl?.locked}
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
|
title={t(
|
||||||
|
"home.settings.plugins.marlin_search.enable_marlin_search",
|
||||||
|
)}
|
||||||
|
onPress={() => {
|
||||||
|
updateSettings({ searchEngine: "Jellyfin" });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={settings.searchEngine === "Marlin"}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
updateSettings({
|
||||||
|
searchEngine: value ? "Marlin" : "Jellyfin",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</DisabledSetting>
|
||||||
|
</ListGroup>
|
||||||
|
|
||||||
|
<DisabledSetting
|
||||||
|
disabled={pluginSettings?.marlinServerUrl?.locked === true}
|
||||||
|
showText={!pluginSettings?.searchEngine?.locked}
|
||||||
|
className='mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}
|
||||||
|
>
|
||||||
|
<Text className='mr-4'>
|
||||||
|
{t("home.settings.plugins.marlin_search.url")}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
editable={settings.searchEngine === "Marlin"}
|
||||||
|
className='text-white'
|
||||||
|
placeholder={t(
|
||||||
|
"home.settings.plugins.marlin_search.server_url_placeholder",
|
||||||
|
)}
|
||||||
|
value={value}
|
||||||
|
keyboardType='url'
|
||||||
|
returnKeyType='done'
|
||||||
|
autoCapitalize='none'
|
||||||
|
textContentType='URL'
|
||||||
|
onChangeText={(text) => setValue(text)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DisabledSetting>
|
||||||
|
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||||
|
{t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "}
|
||||||
|
<Text className='text-blue-500' onPress={handleOpenLink}>
|
||||||
|
{t("home.settings.plugins.marlin_search.read_more_about_marlin")}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</DisabledSetting>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
app/(auth)/(tabs)/(home)/settings/plugins/page.tsx
Normal file
24
app/(auth)/(tabs)/(home)/settings/plugins/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { PluginSettings } from "@/components/settings/PluginSettings";
|
||||||
|
|
||||||
|
export default function PluginsPage() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className='px-4 flex flex-col'
|
||||||
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
|
>
|
||||||
|
<PluginSettings />
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -87,14 +87,15 @@ export default function page() {
|
|||||||
<Text className='font-bold text-2xl mb-1'>{data?.details?.name}</Text>
|
<Text className='font-bold text-2xl mb-1'>{data?.details?.name}</Text>
|
||||||
<Text className='opacity-50'>
|
<Text className='opacity-50'>
|
||||||
{t("jellyseerr.born")}{" "}
|
{t("jellyseerr.born")}{" "}
|
||||||
{new Date(data?.details?.birthday!).toLocaleDateString(
|
{data?.details?.birthday &&
|
||||||
`${locale}-${region}`,
|
new Date(data.details.birthday).toLocaleDateString(
|
||||||
{
|
`${locale}-${region}`,
|
||||||
year: "numeric",
|
{
|
||||||
month: "long",
|
year: "numeric",
|
||||||
day: "numeric",
|
month: "long",
|
||||||
},
|
day: "numeric",
|
||||||
)}{" "}
|
},
|
||||||
|
)}{" "}
|
||||||
| {data?.details?.placeOfBirth}
|
| {data?.details?.placeOfBirth}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export default function page() {
|
|||||||
<View className='flex flex-1'>
|
<View className='flex flex-1'>
|
||||||
<FlashList
|
<FlashList
|
||||||
data={channels?.Items}
|
data={channels?.Items}
|
||||||
estimatedItemSize={76}
|
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<View className='flex flex-row items-center px-4 mb-2'>
|
<View className='flex flex-row items-center px-4 mb-2'>
|
||||||
<View className='w-22 mr-4 rounded-lg overflow-hidden'>
|
<View className='w-22 mr-4 rounded-lg overflow-hidden'>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.2.7/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
|
||||||
"files": {
|
"files": {
|
||||||
"includes": [
|
"includes": [
|
||||||
"**/*",
|
"**/*",
|
||||||
|
|||||||
25
bun.lock
25
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "streamyfin",
|
"name": "streamyfin",
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-collapsible": "^1.6.2",
|
"react-native-collapsible": "^1.6.2",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^15.0.0",
|
||||||
"react-native-edge-to-edge": "^1.7.0",
|
"react-native-edge-to-edge": "^1.7.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-google-cast": "^4.9.1",
|
"react-native-google-cast": "^4.9.1",
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"@biomejs/biome": "^2.2.4",
|
"@biomejs/biome": "^2.3.5",
|
||||||
"@react-native-community/cli": "^20.0.0",
|
"@react-native-community/cli": "^20.0.0",
|
||||||
"@react-native-tvos/config-tv": "^0.1.1",
|
"@react-native-tvos/config-tv": "^0.1.1",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
@@ -300,23 +301,23 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.3.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.4", "@biomejs/cli-darwin-x64": "2.3.4", "@biomejs/cli-linux-arm64": "2.3.4", "@biomejs/cli-linux-arm64-musl": "2.3.4", "@biomejs/cli-linux-x64": "2.3.4", "@biomejs/cli-linux-x64-musl": "2.3.4", "@biomejs/cli-win32-arm64": "2.3.4", "@biomejs/cli-win32-x64": "2.3.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w=="],
|
"@biomejs/biome": ["@biomejs/biome@2.3.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.5", "@biomejs/cli-darwin-x64": "2.3.5", "@biomejs/cli-linux-arm64": "2.3.5", "@biomejs/cli-linux-arm64-musl": "2.3.5", "@biomejs/cli-linux-x64": "2.3.5", "@biomejs/cli-linux-x64-musl": "2.3.5", "@biomejs/cli-win32-arm64": "2.3.5", "@biomejs/cli-win32-x64": "2.3.5" }, "bin": { "biome": "bin/biome" } }, "sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA=="],
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA=="],
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ=="],
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q=="],
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA=="],
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw=="],
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ=="],
|
||||||
|
|
||||||
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@1.0.2", "", { "dependencies": { "color": "^5.0.0" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-OrCw8s2NzFxO1TO5W2vyr7HNvh1Yjy00f72D/0BIPtImc0aj5CRrT9nFRE7YP0FWZb0AY5+0QU9jaoph1rBlSg=="],
|
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@1.0.2", "", { "dependencies": { "color": "^5.0.0" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-OrCw8s2NzFxO1TO5W2vyr7HNvh1Yjy00f72D/0BIPtImc0aj5CRrT9nFRE7YP0FWZb0AY5+0QU9jaoph1rBlSg=="],
|
||||||
|
|
||||||
@@ -1624,7 +1625,7 @@
|
|||||||
|
|
||||||
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
|
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
|
||||||
|
|
||||||
"react-native-device-info": ["react-native-device-info@14.1.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-lXFpe6DJmzbQXNLWxlMHP2xuTU5gwrKAvI8dCAZuERhW9eOXSubOQIesk9lIBnsi9pI19GMrcpJEvs4ARPRYmw=="],
|
"react-native-device-info": ["react-native-device-info@15.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-U5waZRXtT3l1SgZpZMlIvMKPTkFZPH8W7Ks6GrJhdH723aUIPxjVer7cRSij1mvQdOAAYFJV/9BDzlC8apG89A=="],
|
||||||
|
|
||||||
"react-native-edge-to-edge": ["react-native-edge-to-edge@1.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ=="],
|
"react-native-edge-to-edge": ["react-native-edge-to-edge@1.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ=="],
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
|||||||
import { MediaSourceSheet } from "./MediaSourceSheet";
|
import { MediaSourceSheet } from "./MediaSourceSheet";
|
||||||
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
||||||
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
|
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
|
||||||
|
import { RefreshMetadata } from "./RefreshMetadata";
|
||||||
import { TrackSheet } from "./TrackSheet";
|
import { TrackSheet } from "./TrackSheet";
|
||||||
|
|
||||||
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
||||||
@@ -115,7 +116,10 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
|||||||
<DownloadSingleItem item={item} size='large' />
|
<DownloadSingleItem item={item} size='large' />
|
||||||
)}
|
)}
|
||||||
{user?.Policy?.IsAdministrator && (
|
{user?.Policy?.IsAdministrator && (
|
||||||
<PlayInRemoteSessionButton item={item} size='large' />
|
<>
|
||||||
|
<PlayInRemoteSessionButton item={item} size='large' />
|
||||||
|
<RefreshMetadata item={item} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PlayedStatus items={[item]} size='large' />
|
<PlayedStatus items={[item]} size='large' />
|
||||||
@@ -132,7 +136,10 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
|||||||
<DownloadSingleItem item={item} size='large' />
|
<DownloadSingleItem item={item} size='large' />
|
||||||
)}
|
)}
|
||||||
{user?.Policy?.IsAdministrator && (
|
{user?.Policy?.IsAdministrator && (
|
||||||
<PlayInRemoteSessionButton item={item} size='large' />
|
<>
|
||||||
|
<PlayInRemoteSessionButton item={item} size='large' />
|
||||||
|
<RefreshMetadata item={item} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PlayedStatus items={[item]} size='large' />
|
<PlayedStatus items={[item]} size='large' />
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
selectedOptions,
|
selectedOptions,
|
||||||
isOffline,
|
isOffline,
|
||||||
colors,
|
colors,
|
||||||
...props
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const client = useRemoteMediaClient();
|
const client = useRemoteMediaClient();
|
||||||
|
|||||||
38
components/RefreshMetadata.tsx
Normal file
38
components/RefreshMetadata.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { Platform, View, type ViewProps } from "react-native";
|
||||||
|
import { RoundButton } from "@/components/RoundButton";
|
||||||
|
import { useRefreshMetadata } from "@/hooks/useRefreshMetadata";
|
||||||
|
|
||||||
|
interface Props extends ViewProps {
|
||||||
|
item: BaseItemDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefreshMetadata: FC<Props> = ({ item, ...props }) => {
|
||||||
|
const { refreshMetadata, isRefreshing } = useRefreshMetadata(item);
|
||||||
|
|
||||||
|
if (Platform.OS === "ios") {
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<RoundButton
|
||||||
|
size='large'
|
||||||
|
icon='reload-outline'
|
||||||
|
onPress={refreshMetadata}
|
||||||
|
hapticFeedback={!isRefreshing}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<RoundButton
|
||||||
|
size='large'
|
||||||
|
icon='reload-outline'
|
||||||
|
onPress={refreshMetadata}
|
||||||
|
hapticFeedback={!isRefreshing}
|
||||||
|
fillColor={isRefreshing ? "primary" : undefined}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -120,7 +120,6 @@ export function InfiniteHorizontalScroll({
|
|||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<View className='mr-2'>{renderItem(item, index)}</View>
|
<View className='mr-2'>{renderItem(item, index)}</View>
|
||||||
)}
|
)}
|
||||||
estimatedItemSize={height}
|
|
||||||
horizontal
|
horizontal
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
|
|
||||||
const base64Image = useMemo(() => {
|
const base64Image = useMemo(() => {
|
||||||
return storage.getString(item?.Id!);
|
return item?.Id ? storage.getString(item.Id) : undefined;
|
||||||
}, []);
|
}, [item?.Id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deleting the file with haptic feedback.
|
* Handles deleting the file with haptic feedback.
|
||||||
*/
|
*/
|
||||||
const handleDeleteFile = useCallback(() => {
|
const handleDeleteFile = useCallback(() => {
|
||||||
if (item.Id) {
|
if (item.Id) {
|
||||||
deleteFile(item.Id, item.Type);
|
deleteFile(item.Id);
|
||||||
}
|
}
|
||||||
}, [deleteFile, item.Id]);
|
}, [deleteFile, item.Id]);
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ const ParallaxSlideShow = <T,>({
|
|||||||
renderItem={({ item, index }) => renderItem(item, index)}
|
renderItem={({ item, index }) => renderItem(item, index)}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
numColumns={3}
|
numColumns={3}
|
||||||
estimatedItemSize={214}
|
|
||||||
ItemSeparatorComponent={() => <View className='h-2 w-2' />}
|
ItemSeparatorComponent={() => <View className='h-2 w-2' />}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import type { ContentStyle } from "@shopify/flash-list/src/FlashListProps";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { View, type ViewProps } from "react-native";
|
import { View, type ViewProps, type ViewStyle } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||||
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||||
|
|
||||||
export interface SlideProps {
|
export interface SlideProps {
|
||||||
slide: DiscoverSlider;
|
slide: DiscoverSlider;
|
||||||
contentContainerStyle?: ContentStyle;
|
contentContainerStyle?: ViewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<T> extends SlideProps {
|
interface Props<T> extends SlideProps {
|
||||||
@@ -45,7 +44,6 @@ const Slide = <T,>({
|
|||||||
}}
|
}}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
estimatedItemSize={250}
|
|
||||||
data={data}
|
data={data}
|
||||||
onEndReachedThreshold={1}
|
onEndReachedThreshold={1}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export const SearchItemWrapper = <T,>({
|
|||||||
}}
|
}}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
keyExtractor={(_, index) => index.toString()}
|
keyExtractor={(_, index) => index.toString()}
|
||||||
estimatedItemSize={250}
|
|
||||||
data={items}
|
data={items}
|
||||||
onEndReachedThreshold={1}
|
onEndReachedThreshold={1}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ const JellyseerrSeasonEpisodes: React.FC<{
|
|||||||
horizontal
|
horizontal
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
estimatedItemSize={50}
|
|
||||||
data={seasonWithEpisodes?.episodes}
|
data={seasonWithEpisodes?.episodes}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
renderItem={(item, index) => (
|
renderItem={(item, index) => (
|
||||||
@@ -284,7 +283,6 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
ItemSeparatorComponent={() => <View className='h-2' />}
|
ItemSeparatorComponent={() => <View className='h-2' />}
|
||||||
estimatedItemSize={250}
|
|
||||||
renderItem={({ item: season }) => (
|
renderItem={({ item: season }) => (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
|
|||||||
<FlashList
|
<FlashList
|
||||||
contentContainerStyle={{ paddingLeft: 16 }}
|
contentContainerStyle={{ paddingLeft: 16 }}
|
||||||
horizontal
|
horizontal
|
||||||
estimatedItemSize={172}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
data={items}
|
data={items}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
import { TouchableOpacity, type ViewStyle } from "react-native";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
@@ -14,17 +14,20 @@ import {
|
|||||||
} from "../common/HorizontalScroll";
|
} from "../common/HorizontalScroll";
|
||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props {
|
||||||
item?: BaseItemDto | null;
|
item?: BaseItemDto | null;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
isOffline?: boolean;
|
isOffline?: boolean;
|
||||||
|
style?: ViewStyle;
|
||||||
|
containerStyle?: ViewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
loading,
|
loading,
|
||||||
isOffline,
|
isOffline,
|
||||||
...props
|
style,
|
||||||
|
containerStyle,
|
||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
@@ -90,6 +93,8 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
data={episodes}
|
data={episodes}
|
||||||
extraData={item}
|
extraData={item}
|
||||||
loading={loading || isPending}
|
loading={loading || isPending}
|
||||||
|
style={style}
|
||||||
|
containerStyle={containerStyle}
|
||||||
renderItem={(_item, _idx) => (
|
renderItem={(_item, _idx) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={_item.Id}
|
key={_item.Id}
|
||||||
@@ -104,7 +109,6 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
<ItemCardText item={_item} />
|
<ItemCardText item={_item} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View, type ViewProps } from "react-native";
|
import { Platform, View, type ViewProps } from "react-native";
|
||||||
@@ -50,12 +51,17 @@ export const AppLanguageSelector: React.FC<Props> = () => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={optionGroups}
|
groups={optionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text>
|
<Text className='mr-2'>
|
||||||
{APP_LANGUAGES.find(
|
{APP_LANGUAGES.find(
|
||||||
(l) => l.value === settings?.preferedLanguage,
|
(l) => l.value === settings?.preferedLanguage,
|
||||||
)?.label || t("home.settings.languages.system")}
|
)?.label || t("home.settings.languages.system")}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name='chevron-expand-sharp'
|
||||||
|
size={18}
|
||||||
|
color='#5A5960'
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={t("home.settings.languages.title")}
|
title={t("home.settings.languages.title")}
|
||||||
|
|||||||
63
components/settings/AppearanceSettings.tsx
Normal file
63
components/settings/AppearanceSettings.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import type React from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Linking, Switch } from "react-native";
|
||||||
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
|
export const AppearanceSettings: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const disabled = useMemo(
|
||||||
|
() =>
|
||||||
|
pluginSettings?.showCustomMenuLinks?.locked === true &&
|
||||||
|
pluginSettings?.hiddenLibraries?.locked === true,
|
||||||
|
[pluginSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisabledSetting disabled={disabled}>
|
||||||
|
<ListGroup title={t("home.settings.appearance.title")} className=''>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.show_custom_menu_links")}
|
||||||
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
|
onPress={() =>
|
||||||
|
Linking.openURL(
|
||||||
|
"https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={settings.showCustomMenuLinks}
|
||||||
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ showCustomMenuLinks: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem title={t("home.settings.other.show_large_home_carousel")}>
|
||||||
|
<Switch
|
||||||
|
value={settings.showLargeHomeCarousel}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ showLargeHomeCarousel: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
onPress={() =>
|
||||||
|
router.push("/settings/appearance/hide-libraries/page")
|
||||||
|
}
|
||||||
|
title={t("home.settings.other.hide_libraries")}
|
||||||
|
showArrow
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
</DisabledSetting>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -83,7 +83,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={optionGroups}
|
groups={optionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{settings?.defaultAudioLanguage?.DisplayName ||
|
{settings?.defaultAudioLanguage?.DisplayName ||
|
||||||
t("home.settings.audio.none")}
|
t("home.settings.audio.none")}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={orientationOptions}
|
groups={orientationOptions}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(
|
{t(
|
||||||
orientationTranslations[
|
orientationTranslations[
|
||||||
@@ -152,7 +152,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
keyExtractor={String}
|
keyExtractor={String}
|
||||||
titleExtractor={(item) => t(`home.settings.other.video_players.${VideoPlayer[item]}`)}
|
titleExtractor={(item) => t(`home.settings.other.video_players.${VideoPlayer[item]}`)}
|
||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-1.5 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)}
|
{t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -208,7 +208,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={bitrateOptions}
|
groups={bitrateOptions}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{settings.defaultBitrate?.key}
|
{settings.defaultBitrate?.key}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -238,7 +238,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={autoPlayEpisodeOptions}
|
groups={autoPlayEpisodeOptions}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(settings?.maxAutoPlayEpisodeCount.key)}
|
{t(settings?.maxAutoPlayEpisodeCount.key)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
211
components/settings/PlaybackControlsSettings.tsx
Normal file
211
components/settings/PlaybackControlsSettings.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
import type React from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Switch, View } from "react-native";
|
||||||
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
|
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
|
export const PlaybackControlsSettings: React.FC = () => {
|
||||||
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const disabled = useMemo(
|
||||||
|
() =>
|
||||||
|
pluginSettings?.defaultVideoOrientation?.locked === true &&
|
||||||
|
pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
|
||||||
|
pluginSettings?.disableHapticFeedback?.locked === true,
|
||||||
|
[pluginSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orientations = [
|
||||||
|
ScreenOrientation.OrientationLock.DEFAULT,
|
||||||
|
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const orientationTranslations = useMemo(
|
||||||
|
() => ({
|
||||||
|
[ScreenOrientation.OrientationLock.DEFAULT]:
|
||||||
|
"home.settings.other.orientations.DEFAULT",
|
||||||
|
[ScreenOrientation.OrientationLock.PORTRAIT_UP]:
|
||||||
|
"home.settings.other.orientations.PORTRAIT_UP",
|
||||||
|
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]:
|
||||||
|
"home.settings.other.orientations.LANDSCAPE_LEFT",
|
||||||
|
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]:
|
||||||
|
"home.settings.other.orientations.LANDSCAPE_RIGHT",
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orientationOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
options: orientations.map((orientation) => ({
|
||||||
|
type: "radio" as const,
|
||||||
|
label: t(ScreenOrientationEnum[orientation]),
|
||||||
|
value: String(orientation),
|
||||||
|
selected: orientation === settings?.defaultVideoOrientation,
|
||||||
|
onPress: () =>
|
||||||
|
updateSettings({ defaultVideoOrientation: orientation }),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[orientations, settings?.defaultVideoOrientation, t, updateSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
const bitrateOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
options: BITRATES.map((bitrate) => ({
|
||||||
|
type: "radio" as const,
|
||||||
|
label: bitrate.key,
|
||||||
|
value: bitrate.key,
|
||||||
|
selected: bitrate.key === settings?.defaultBitrate?.key,
|
||||||
|
onPress: () => updateSettings({ defaultBitrate: bitrate }),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[settings?.defaultBitrate?.key, updateSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoPlayEpisodeOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
options: AUTOPLAY_EPISODES_COUNT(t).map((item) => ({
|
||||||
|
type: "radio" as const,
|
||||||
|
label: item.key,
|
||||||
|
value: item.key,
|
||||||
|
selected: item.key === settings?.maxAutoPlayEpisodeCount?.key,
|
||||||
|
onPress: () => updateSettings({ maxAutoPlayEpisodeCount: item }),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[settings?.maxAutoPlayEpisodeCount?.key, t, updateSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisabledSetting disabled={disabled}>
|
||||||
|
<ListGroup title={t("home.settings.other.other_title")} className=''>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.video_orientation")}
|
||||||
|
disabled={pluginSettings?.defaultVideoOrientation?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
groups={orientationOptions}
|
||||||
|
trigger={
|
||||||
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
|
{t(
|
||||||
|
orientationTranslations[
|
||||||
|
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>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.safe_area_in_controls")}
|
||||||
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={settings.safeAreaInControlsEnabled}
|
||||||
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ safeAreaInControlsEnabled: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.default_quality")}
|
||||||
|
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||||
|
>
|
||||||
|
<PlatformDropdown
|
||||||
|
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>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.disable_haptic_feedback")}
|
||||||
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={settings.disableHapticFeedback}
|
||||||
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
|
onValueChange={(disableHapticFeedback) =>
|
||||||
|
updateSettings({ disableHapticFeedback })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
|
||||||
|
<PlatformDropdown
|
||||||
|
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>
|
||||||
|
</DisabledSetting>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTOPLAY_EPISODES_COUNT = (
|
||||||
|
t: TFunction<"translation", undefined>,
|
||||||
|
): {
|
||||||
|
key: string;
|
||||||
|
value: number;
|
||||||
|
}[] => [
|
||||||
|
{ key: t("home.settings.other.disabled"), value: -1 },
|
||||||
|
{ key: "1", value: 1 },
|
||||||
|
{ key: "2", value: 2 },
|
||||||
|
{ key: "3", value: 3 },
|
||||||
|
{ key: "4", value: 4 },
|
||||||
|
{ key: "5", value: 5 },
|
||||||
|
{ key: "6", value: 6 },
|
||||||
|
{ key: "7", value: 7 },
|
||||||
|
];
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
|
||||||
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";
|
import { ListItem } from "../list/ListItem";
|
||||||
@@ -13,23 +12,22 @@ export const PluginSettings = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='mt-4'>
|
<ListGroup
|
||||||
<ListGroup
|
title={t("home.settings.plugins.plugins_title")}
|
||||||
title={t("home.settings.plugins.plugins_title")}
|
className='mb-4'
|
||||||
className='mb-4'
|
>
|
||||||
>
|
<ListItem
|
||||||
<ListItem
|
onPress={() => router.push("/settings/plugins/jellyseerr/page")}
|
||||||
onPress={() => router.push("/settings/jellyseerr/page")}
|
title={"Jellyseerr"}
|
||||||
title={"Jellyseerr"}
|
showArrow
|
||||||
showArrow
|
/>
|
||||||
/>
|
<ListItem
|
||||||
<ListItem
|
onPress={() => router.push("/settings/plugins/marlin-search/page")}
|
||||||
onPress={() => router.push("/settings/marlin-search/page")}
|
title='Marlin Search'
|
||||||
title='Marlin Search'
|
showArrow
|
||||||
showArrow
|
/>
|
||||||
/>
|
</ListGroup>
|
||||||
</ListGroup>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={subtitleLanguageOptionGroups}
|
groups={subtitleLanguageOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{settings?.defaultSubtitleLanguage?.DisplayName ||
|
{settings?.defaultSubtitleLanguage?.DisplayName ||
|
||||||
t("home.settings.subtitles.none")}
|
t("home.settings.subtitles.none")}
|
||||||
@@ -210,7 +210,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={subtitleModeOptionGroups}
|
groups={subtitleModeOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(subtitleModeKeys[settings?.subtitleMode]) ||
|
{t(subtitleModeKeys[settings?.subtitleMode]) ||
|
||||||
t("home.settings.subtitles.loading")}
|
t("home.settings.subtitles.loading")}
|
||||||
@@ -256,7 +256,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={textColorOptionGroups}
|
groups={textColorOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(
|
{t(
|
||||||
`home.settings.subtitles.colors.${settings?.vlcTextColor || "White"}`,
|
`home.settings.subtitles.colors.${settings?.vlcTextColor || "White"}`,
|
||||||
@@ -276,7 +276,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={backgroundColorOptionGroups}
|
groups={backgroundColorOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(
|
{t(
|
||||||
`home.settings.subtitles.colors.${settings?.vlcBackgroundColor || "Black"}`,
|
`home.settings.subtitles.colors.${settings?.vlcBackgroundColor || "Black"}`,
|
||||||
@@ -296,7 +296,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={outlineColorOptionGroups}
|
groups={outlineColorOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(
|
{t(
|
||||||
`home.settings.subtitles.colors.${settings?.vlcOutlineColor || "Black"}`,
|
`home.settings.subtitles.colors.${settings?.vlcOutlineColor || "Black"}`,
|
||||||
@@ -316,7 +316,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={outlineThicknessOptionGroups}
|
groups={outlineThicknessOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
<Text className='mr-1 text-[#8E8D91]'>
|
||||||
{t(
|
{t(
|
||||||
`home.settings.subtitles.thickness.${settings?.vlcOutlineThickness || "Normal"}`,
|
`home.settings.subtitles.thickness.${settings?.vlcOutlineThickness || "Normal"}`,
|
||||||
@@ -336,7 +336,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={backgroundOpacityOptionGroups}
|
groups={backgroundOpacityOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>{`${Math.round(((settings?.vlcBackgroundOpacity ?? 128) / 255) * 100)}%`}</Text>
|
<Text className='mr-1 text-[#8E8D91]'>{`${Math.round(((settings?.vlcBackgroundOpacity ?? 128) / 255) * 100)}%`}</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name='chevron-expand-sharp'
|
name='chevron-expand-sharp'
|
||||||
@@ -352,7 +352,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={outlineOpacityOptionGroups}
|
groups={outlineOpacityOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||||
<Text className='mr-1 text-[#8E8D91]'>{`${Math.round(((settings?.vlcOutlineOpacity ?? 255) / 255) * 100)}%`}</Text>
|
<Text className='mr-1 text-[#8E8D91]'>{`${Math.round(((settings?.vlcOutlineOpacity ?? 255) / 255) * 100)}%`}</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name='chevron-expand-sharp'
|
name='chevron-expand-sharp'
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(e: BaseItemDto) => e.Id ?? ""}
|
keyExtractor={(e: BaseItemDto) => e.Id ?? ""}
|
||||||
estimatedItemSize={200}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -89,10 +89,10 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
|||||||
<Image
|
<Image
|
||||||
cachePolicy={"memory-disk"}
|
cachePolicy={"memory-disk"}
|
||||||
style={{
|
style={{
|
||||||
width: 150 * trickplayInfo?.data.TileWidth!,
|
width: 150 * trickplayInfo.data.TileWidth,
|
||||||
height:
|
height:
|
||||||
(150 / trickplayInfo.aspectRatio!) *
|
(150 / trickplayInfo.aspectRatio) *
|
||||||
trickplayInfo?.data.TileHeight!,
|
trickplayInfo.data.TileHeight,
|
||||||
transform: [
|
transform: [
|
||||||
{ translateX: -x * tileWidth },
|
{ translateX: -x * tileWidth },
|
||||||
{ translateY: -y * tileHeight },
|
{ translateY: -y * tileHeight },
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
|
|||||||
<Image
|
<Image
|
||||||
cachePolicy={"memory-disk"}
|
cachePolicy={"memory-disk"}
|
||||||
style={{
|
style={{
|
||||||
width: tileWidth * trickplayInfo?.data.TileWidth!,
|
width: tileWidth * (trickplayInfo.data.TileWidth ?? 1),
|
||||||
height:
|
height:
|
||||||
(tileWidth / trickplayInfo.aspectRatio!) *
|
(tileWidth / (trickplayInfo.aspectRatio ?? 1)) *
|
||||||
trickplayInfo?.data.TileHeight!,
|
(trickplayInfo.data.TileHeight ?? 1),
|
||||||
transform: [
|
transform: [
|
||||||
{ translateX: -x * tileWidth },
|
{ translateX: -x * tileWidth },
|
||||||
{ translateY: -y * tileHeight },
|
{ translateY: -y * tileHeight },
|
||||||
|
|||||||
42
hooks/useRefreshMetadata.ts
Normal file
42
hooks/useRefreshMetadata.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { getItemRefreshApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|
||||||
|
export const useRefreshMetadata = (item: BaseItemDto) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const type = "item";
|
||||||
|
|
||||||
|
const refreshMetadataMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (api && item.Id) {
|
||||||
|
await getItemRefreshApi(api).refreshItem({
|
||||||
|
itemId: item.Id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("metadata.refresh_triggered"));
|
||||||
|
queryClient.invalidateQueries({ queryKey: [type, item.Id] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to refresh metadata:", error);
|
||||||
|
toast.error(t("metadata.refresh_failed"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshMetadata = () => {
|
||||||
|
refreshMetadataMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
refreshMetadata,
|
||||||
|
isRefreshing: refreshMetadataMutation.isPending,
|
||||||
|
refreshMetadataMutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -31,7 +31,9 @@ export interface ActiveDownload {
|
|||||||
|
|
||||||
export interface BackgroundDownloaderModuleType {
|
export interface BackgroundDownloaderModuleType {
|
||||||
startDownload(url: string, destinationPath?: string): Promise<number>;
|
startDownload(url: string, destinationPath?: string): Promise<number>;
|
||||||
|
enqueueDownload(url: string, destinationPath?: string): Promise<number>;
|
||||||
cancelDownload(taskId: number): void;
|
cancelDownload(taskId: number): void;
|
||||||
|
cancelQueuedDownload(url: string): void;
|
||||||
cancelAllDownloads(): void;
|
cancelAllDownloads(): void;
|
||||||
getActiveDownloads(): Promise<ActiveDownload[]>;
|
getActiveDownloads(): Promise<ActiveDownload[]>;
|
||||||
addListener(
|
addListener(
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-collapsible": "^1.6.2",
|
"react-native-collapsible": "^1.6.2",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^15.0.0",
|
||||||
"react-native-edge-to-edge": "^1.7.0",
|
"react-native-edge-to-edge": "^1.7.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-google-cast": "^4.9.1",
|
"react-native-google-cast": "^4.9.1",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"@biomejs/biome": "^2.2.4",
|
"@biomejs/biome": "^2.3.5",
|
||||||
"@react-native-community/cli": "^20.0.0",
|
"@react-native-community/cli": "^20.0.0",
|
||||||
"@react-native-tvos/config-tv": "^0.1.1",
|
"@react-native-tvos/config-tv": "^0.1.1",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getAllDownloadedItems,
|
getAllDownloadedItems,
|
||||||
getDownloadedItemById,
|
getDownloadedItemById,
|
||||||
getDownloadsDatabase,
|
getDownloadsDatabase,
|
||||||
|
updateDownloadedItem,
|
||||||
} from "./Downloads/database";
|
} from "./Downloads/database";
|
||||||
import { getDownloadedItemSize } from "./Downloads/fileOperations";
|
import { getDownloadedItemSize } from "./Downloads/fileOperations";
|
||||||
import { useDownloadEventHandlers } from "./Downloads/hooks/useDownloadEventHandlers";
|
import { useDownloadEventHandlers } from "./Downloads/hooks/useDownloadEventHandlers";
|
||||||
@@ -29,7 +30,7 @@ function useDownloadProvider() {
|
|||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
// Track task ID to process ID mapping
|
// Track task ID to process ID mapping
|
||||||
const taskMapRef = useRef<Map<number, string>>(new Map());
|
const taskMapRef = useRef<Map<number | string, string>>(new Map());
|
||||||
|
|
||||||
// Reactive downloaded items that updates when refreshKey changes
|
// Reactive downloaded items that updates when refreshKey changes
|
||||||
const downloadedItems = useMemo(() => {
|
const downloadedItems = useMemo(() => {
|
||||||
@@ -130,13 +131,13 @@ function useDownloadProvider() {
|
|||||||
cancelDownload,
|
cancelDownload,
|
||||||
getDownloadedItemSize,
|
getDownloadedItemSize,
|
||||||
getDownloadedItemById,
|
getDownloadedItemById,
|
||||||
|
updateDownloadedItem,
|
||||||
triggerRefresh,
|
triggerRefresh,
|
||||||
APP_CACHE_DOWNLOAD_DIRECTORY: APP_CACHE_DOWNLOAD_DIRECTORY.uri,
|
APP_CACHE_DOWNLOAD_DIRECTORY: APP_CACHE_DOWNLOAD_DIRECTORY.uri,
|
||||||
appSizeUsage,
|
appSizeUsage,
|
||||||
// Deprecated/not implemented in simple version
|
// Deprecated/not implemented in simple version
|
||||||
startDownload: async () => {},
|
startDownload: async () => {},
|
||||||
cleanCacheDirectory: async () => {},
|
cleanCacheDirectory: async () => {},
|
||||||
updateDownloadedItem: () => {},
|
|
||||||
dumpDownloadDiagnostics: async () => "",
|
dumpDownloadDiagnostics: async () => "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -161,9 +162,9 @@ export function useDownload() {
|
|||||||
startDownload: async () => {},
|
startDownload: async () => {},
|
||||||
getDownloadedItemSize: () => 0,
|
getDownloadedItemSize: () => 0,
|
||||||
getDownloadedItemById: () => undefined,
|
getDownloadedItemById: () => undefined,
|
||||||
|
updateDownloadedItem: () => {},
|
||||||
APP_CACHE_DOWNLOAD_DIRECTORY: "",
|
APP_CACHE_DOWNLOAD_DIRECTORY: "",
|
||||||
cleanCacheDirectory: async () => {},
|
cleanCacheDirectory: async () => {},
|
||||||
updateDownloadedItem: () => {},
|
|
||||||
appSizeUsage: async () => ({ total: 0, remaining: 0, appSize: 0 }),
|
appSizeUsage: async () => ({ total: 0, remaining: 0, appSize: 0 }),
|
||||||
dumpDownloadDiagnostics: async () => "",
|
dumpDownloadDiagnostics: async () => "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -185,10 +185,16 @@ export async function fetchSegments(
|
|||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const segments = await fetchAndParseSegments(itemId, api);
|
const segments = await fetchAndParseSegments(itemId, api);
|
||||||
return segments;
|
return {
|
||||||
|
introSegments: segments.introSegments,
|
||||||
|
creditSegments: segments.creditSegments,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[SEGMENTS] Failed to fetch segments:`, error);
|
console.error(`[SEGMENTS] Failed to fetch segments:`, error);
|
||||||
return {};
|
return {
|
||||||
|
introSegments: undefined,
|
||||||
|
creditSegments: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +228,12 @@ export async function downloadAdditionalAssets(params: {
|
|||||||
mediaSource.TranscodingUrl
|
mediaSource.TranscodingUrl
|
||||||
? Promise.resolve(mediaSource)
|
? Promise.resolve(mediaSource)
|
||||||
: downloadSubtitles(mediaSource, item, api.basePath || ""),
|
: downloadSubtitles(mediaSource, item, api.basePath || ""),
|
||||||
item.Id ? fetchSegments(item.Id, api) : Promise.resolve({}),
|
item.Id
|
||||||
|
? fetchSegments(item.Id, api)
|
||||||
|
: Promise.resolve({
|
||||||
|
introSegments: undefined,
|
||||||
|
creditSegments: undefined,
|
||||||
|
}),
|
||||||
// Cover image downloads (run but don't wait for results)
|
// Cover image downloads (run but don't wait for results)
|
||||||
downloadCoverImage(item, api, saveImageFn).catch((err) => {
|
downloadCoverImage(item, api, saveImageFn).catch((err) => {
|
||||||
console.error("[COVER] Error downloading cover:", err);
|
console.error("[COVER] Error downloading cover:", err);
|
||||||
|
|||||||
@@ -181,6 +181,41 @@ export function removeDownloadedItem(id: string): DownloadedItem | undefined {
|
|||||||
return itemToDelete;
|
return itemToDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a downloaded item in the database
|
||||||
|
*/
|
||||||
|
export function updateDownloadedItem(
|
||||||
|
_id: string,
|
||||||
|
updatedItem: DownloadedItem,
|
||||||
|
): void {
|
||||||
|
const db = getDownloadsDatabase();
|
||||||
|
const baseItem = updatedItem.item;
|
||||||
|
|
||||||
|
if (baseItem.Type === "Movie" && baseItem.Id) {
|
||||||
|
db.movies[baseItem.Id] = updatedItem;
|
||||||
|
} else if (
|
||||||
|
baseItem.Type === "Episode" &&
|
||||||
|
baseItem.SeriesId &&
|
||||||
|
baseItem.ParentIndexNumber !== undefined &&
|
||||||
|
baseItem.ParentIndexNumber !== null &&
|
||||||
|
baseItem.IndexNumber !== undefined &&
|
||||||
|
baseItem.IndexNumber !== null
|
||||||
|
) {
|
||||||
|
const seriesId = baseItem.SeriesId;
|
||||||
|
const seasonNumber = baseItem.ParentIndexNumber;
|
||||||
|
const episodeNumber = baseItem.IndexNumber;
|
||||||
|
|
||||||
|
if (db.series[seriesId]?.seasons[seasonNumber]?.episodes[episodeNumber]) {
|
||||||
|
db.series[seriesId].seasons[seasonNumber].episodes[episodeNumber] =
|
||||||
|
updatedItem;
|
||||||
|
}
|
||||||
|
} else if (baseItem.Id && db.other?.[baseItem.Id]) {
|
||||||
|
db.other[baseItem.Id] = updatedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDownloadsDatabase(db);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all downloaded items from the database
|
* Clear all downloaded items from the database
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
} from "./useDownloadSpeedCalculator";
|
} from "./useDownloadSpeedCalculator";
|
||||||
|
|
||||||
interface UseDownloadEventHandlersProps {
|
interface UseDownloadEventHandlersProps {
|
||||||
taskMapRef: MutableRefObject<Map<number, string>>;
|
taskMapRef: MutableRefObject<Map<number | string, string>>;
|
||||||
processes: JobStatus[];
|
processes: JobStatus[];
|
||||||
updateProcess: (
|
updateProcess: (
|
||||||
processId: string,
|
processId: string,
|
||||||
@@ -59,7 +59,8 @@ export function useDownloadEventHandlers({
|
|||||||
// If no mapping exists, find by URL (for queued downloads)
|
// If no mapping exists, find by URL (for queued downloads)
|
||||||
if (!processId && event.url) {
|
if (!processId && event.url) {
|
||||||
// Check if we have a URL mapping (queued download)
|
// Check if we have a URL mapping (queued download)
|
||||||
processId = taskMapRef.current.get(event.url);
|
const urlKey = event.url;
|
||||||
|
processId = taskMapRef.current.get(urlKey);
|
||||||
|
|
||||||
if (!processId) {
|
if (!processId) {
|
||||||
// Fallback: search by matching URL in processes
|
// Fallback: search by matching URL in processes
|
||||||
@@ -74,7 +75,7 @@ export function useDownloadEventHandlers({
|
|||||||
if (processId) {
|
if (processId) {
|
||||||
// Create taskId mapping and remove URL mapping
|
// Create taskId mapping and remove URL mapping
|
||||||
taskMapRef.current.set(event.taskId, processId);
|
taskMapRef.current.set(event.taskId, processId);
|
||||||
taskMapRef.current.delete(event.url);
|
taskMapRef.current.delete(urlKey);
|
||||||
console.log(
|
console.log(
|
||||||
`[DPL] Mapped queued download: taskId=${event.taskId} to processId=${processId.slice(0, 8)}...`,
|
`[DPL] Mapped queued download: taskId=${event.taskId} to processId=${processId.slice(0, 8)}...`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import type { JobStatus } from "../types";
|
|||||||
import { generateFilename, uriToFilePath } from "../utils";
|
import { generateFilename, uriToFilePath } from "../utils";
|
||||||
|
|
||||||
interface UseDownloadOperationsProps {
|
interface UseDownloadOperationsProps {
|
||||||
taskMapRef: MutableRefObject<Map<number, string>>;
|
taskMapRef: MutableRefObject<Map<number | string, string>>;
|
||||||
processes: JobStatus[];
|
processes: JobStatus[];
|
||||||
setProcesses: (updater: (prev: JobStatus[]) => JobStatus[]) => void;
|
setProcesses: (updater: (prev: JobStatus[]) => JobStatus[]) => void;
|
||||||
removeProcess: (id: string) => void;
|
removeProcess: (id: string) => void;
|
||||||
@@ -169,7 +169,7 @@ export function useDownloadOperations({
|
|||||||
if (typeof key === "number") {
|
if (typeof key === "number") {
|
||||||
taskId = key;
|
taskId = key;
|
||||||
} else {
|
} else {
|
||||||
downloadUrl = key;
|
downloadUrl = key as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
console.log(`${data?.url?.slice(0, 100)}...${data?.url?.slice(-50)}`);
|
console.log(`${data?.url?.slice(0, 100)}...${data?.url?.slice(-50)}`);
|
||||||
|
|
||||||
_setPlaySettings(newSettings);
|
_setPlaySettings(newSettings);
|
||||||
setPlayUrl(data?.url!);
|
if (data?.url) setPlayUrl(data.url);
|
||||||
setPlaySessionId(data?.sessionId!);
|
if (data?.sessionId) setPlaySessionId(data.sessionId);
|
||||||
setMediaSource(data?.mediaSource!);
|
if (data?.mediaSource) setMediaSource(data.mediaSource);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -69,6 +69,18 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Settings",
|
"settings_title": "Settings",
|
||||||
"log_out_button": "Log Out",
|
"log_out_button": "Log Out",
|
||||||
|
"categories": {
|
||||||
|
"title": "Categories"
|
||||||
|
},
|
||||||
|
"playback_controls": {
|
||||||
|
"title": "Playback & Controls"
|
||||||
|
},
|
||||||
|
"audio_subtitles": {
|
||||||
|
"title": "Audio & Subtitles"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance"
|
||||||
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"user_info_title": "User Info",
|
"user_info_title": "User Info",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
@@ -236,6 +248,7 @@
|
|||||||
"delete_all_downloaded_files": "Delete All Downloaded Files"
|
"delete_all_downloaded_files": "Delete All Downloaded Files"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
|
"title": "Intro",
|
||||||
"show_intro": "Show Intro",
|
"show_intro": "Show Intro",
|
||||||
"reset_intro": "Reset Intro"
|
"reset_intro": "Reset Intro"
|
||||||
},
|
},
|
||||||
@@ -515,5 +528,9 @@
|
|||||||
"library": "Library",
|
"library": "Library",
|
||||||
"custom_links": "Custom Links",
|
"custom_links": "Custom Links",
|
||||||
"favorites": "Favorites"
|
"favorites": "Favorites"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"refresh_triggered": "Metadata refresh triggered",
|
||||||
|
"refresh_failed": "Failed to refresh metadata"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user