diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index de5545d62..1a09e5c78 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -243,6 +243,28 @@ export default function IndexLayout() { headerLeft: () => , }} /> + , + }} + /> + , + }} + /> {Object.entries(nestedTabPageScreenOptions).map(([name, options]) => ( ))} diff --git a/app/(auth)/(tabs)/(home)/settings/account/page.tsx b/app/(auth)/(tabs)/(home)/settings/account/page.tsx new file mode 100644 index 000000000..2e2a1d314 --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/account/page.tsx @@ -0,0 +1,53 @@ +import * as Application from "expo-application"; +import { setStringAsync } from "expo-clipboard"; +import { t } from "i18next"; +import { useAtom } from "jotai"; +import { useState } from "react"; +import { Alert, ScrollView } from "react-native"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import { useHaptic } from "@/hooks/useHaptic"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; + +export default function AccountPage() { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const [revealed, setRevealed] = useState(false); + const success = useHaptic("success"); + const version = Application.nativeApplicationVersion ?? "N/A"; + const token = api?.accessToken ?? ""; + const masked = token ? `•••• •••• •••• ${token.slice(-4)}` : ""; + + return ( + + + + + setRevealed((r) => !r)} + /> + { + await setStringAsync(token); + success(); + Alert.alert(t("home.settings.account.copied")); + }} + /> + + + + ); +} diff --git a/app/(auth)/(tabs)/(home)/settings/notifications/page.tsx b/app/(auth)/(tabs)/(home)/settings/notifications/page.tsx new file mode 100644 index 000000000..8ba093d3d --- /dev/null +++ b/app/(auth)/(tabs)/(home)/settings/notifications/page.tsx @@ -0,0 +1,61 @@ +import * as Notifications from "expo-notifications"; +import { t } from "i18next"; +import { useEffect, useState } from "react"; +import { Linking, ScrollView, Switch } from "react-native"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import { useSettings } from "@/utils/atoms/settings"; + +export default function NotificationsPage() { + const { settings, updateSettings } = useSettings(); + const [granted, setGranted] = useState(null); + + useEffect(() => { + Notifications.getPermissionsAsync().then((p) => setGranted(p.granted)); + }, []); + + const requestPermission = async () => { + const p = await Notifications.requestPermissionsAsync(); + setGranted(p.granted); + if (!p.granted) Linking.openSettings(); + }; + + if (!settings) return null; + + return ( + + + + + + + updateSettings({ notificationsEnabled: v })} + /> + + + updateSettings({ notifyDownloads: v })} + /> + + + + ); +} diff --git a/providers/Downloads/notifications.ts b/providers/Downloads/notifications.ts index d2d1a7a13..79bb8e1d8 100644 --- a/providers/Downloads/notifications.ts +++ b/providers/Downloads/notifications.ts @@ -2,6 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type * as NotificationsType from "expo-notifications"; import type { TFunction } from "i18next"; import { Platform } from "react-native"; +import { storage } from "@/utils/mmkv"; // Conditionally import expo-notifications only on non-TV platforms const Notifications = Platform.isTV @@ -67,6 +68,14 @@ export async function sendDownloadNotification( ): Promise { if (Platform.isTV || !Notifications) return; + try { + const raw = storage.getString("settings"); + const s = raw ? JSON.parse(raw) : {}; + if (s.notificationsEnabled === false || s.notifyDownloads === false) return; + } catch { + // ignore parse errors; fall through to sending (defaults are enabled) + } + try { await Notifications.scheduleNotificationAsync({ content: {