feat(settings): add Account and Notifications pages, gate download notifications

This commit is contained in:
Gauvain
2026-06-03 23:36:39 +02:00
parent a0e6b31500
commit e783227ba6
4 changed files with 145 additions and 0 deletions

View File

@@ -243,6 +243,28 @@ export default function IndexLayout() {
headerLeft: () => <HeaderBackButton />,
}}
/>
<Stack.Screen
name='settings/account/page'
options={{
title: t("home.settings.account.title"),
headerShown: !Platform.isTV,
headerBlurEffect: "none",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
headerLeft: () => <HeaderBackButton />,
}}
/>
<Stack.Screen
name='settings/notifications/page'
options={{
title: t("home.settings.notifications.title"),
headerShown: !Platform.isTV,
headerBlurEffect: "none",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
headerLeft: () => <HeaderBackButton />,
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}

View File

@@ -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 (
<ScrollView contentContainerStyle={{ padding: 16 }}>
<ListGroup title={t("home.settings.user_info.user_info_title")}>
<ListItem
title={t("home.settings.user_info.user")}
value={user?.Name}
/>
<ListItem
title={t("home.settings.user_info.server")}
value={api?.basePath}
/>
<ListItem
title={t("home.settings.user_info.token")}
value={revealed ? token : masked}
onPress={() => setRevealed((r) => !r)}
/>
<ListItem
title={t("home.settings.account.copy_token")}
textColor='blue'
onPress={async () => {
await setStringAsync(token);
success();
Alert.alert(t("home.settings.account.copied"));
}}
/>
<ListItem
title={t("home.settings.user_info.app_version")}
value={version}
/>
</ListGroup>
</ScrollView>
);
}

View File

@@ -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<boolean | null>(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 (
<ScrollView contentContainerStyle={{ padding: 16 }}>
<ListGroup title={t("home.settings.notifications.permission_title")}>
<ListItem
title={t("home.settings.notifications.system_permission")}
value={
granted == null
? "…"
: granted
? t("home.settings.notifications.granted")
: t("home.settings.notifications.denied")
}
textColor={granted ? "default" : "blue"}
onPress={granted ? undefined : requestPermission}
/>
</ListGroup>
<ListGroup
title={t("home.settings.notifications.events_title")}
className='mt-4'
>
<ListItem title={t("home.settings.notifications.master")}>
<Switch
value={settings.notificationsEnabled}
onValueChange={(v) => updateSettings({ notificationsEnabled: v })}
/>
</ListItem>
<ListItem title={t("home.settings.notifications.downloads")}>
<Switch
value={settings.notifyDownloads}
disabled={!settings.notificationsEnabled}
onValueChange={(v) => updateSettings({ notifyDownloads: v })}
/>
</ListItem>
</ListGroup>
</ScrollView>
);
}

View File

@@ -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<void> {
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: {