325 lines
9.9 KiB
TypeScript
325 lines
9.9 KiB
TypeScript
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
|
import { useFocusEffect } from "expo-router";
|
|
import { Alert, Linking, Platform, ScrollView, Text, View } from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
import { useCallback, useState } from "react";
|
|
|
|
import { SettingsRow } from "@/components/settings-row";
|
|
import {
|
|
type Lang,
|
|
type ThemeMode,
|
|
useSettingsStore,
|
|
} from "@/lib/settings";
|
|
import { useT } from "@/lib/i18n";
|
|
import { useLocationPermission } from "@/lib/use-location-permission";
|
|
import { useTheme } from "@/lib/theme";
|
|
|
|
type IconName = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
|
|
|
const SectionHeader = ({ title }: { title: string }) => (
|
|
<Text className="text-xs font-JakartaSemiBold uppercase tracking-wide text-general-200 dark:text-neutral-500 mt-6 mb-2 px-1">
|
|
{title}
|
|
</Text>
|
|
);
|
|
|
|
const Card = ({ children }: { children: React.ReactNode }) => (
|
|
<View className="rounded-2xl bg-white dark:bg-neutral-900 overflow-hidden shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40">
|
|
{children}
|
|
</View>
|
|
);
|
|
|
|
const Settings = () => {
|
|
const t = useT();
|
|
const { isDark } = useTheme();
|
|
|
|
const mode = useSettingsStore((state) => state.mode);
|
|
const setMode = useSettingsStore((state) => state.setMode);
|
|
const lang = useSettingsStore((state) => state.lang);
|
|
const setLang = useSettingsStore((state) => state.setLang);
|
|
const keepAwake = useSettingsStore((state) => state.keepAwake);
|
|
const setKeepAwake = useSettingsStore((state) => state.setKeepAwake);
|
|
const overlayRequested = useSettingsStore(
|
|
(state) => state.overlayRequested,
|
|
);
|
|
const setOverlayRequested = useSettingsStore(
|
|
(state) => state.setOverlayRequested,
|
|
);
|
|
|
|
const { status, refresh, openSettings } = useLocationPermission();
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
void refresh();
|
|
}, [refresh]),
|
|
);
|
|
|
|
const [expandedSafety, setExpandedSafety] = useState<string | null>(null);
|
|
|
|
const locationStatusLabel =
|
|
status === "granted"
|
|
? t("settings.maps.statusGranted")
|
|
: status === "denied"
|
|
? t("settings.maps.statusDenied")
|
|
: status === "blocked"
|
|
? t("settings.maps.statusBlocked")
|
|
: t("settings.maps.statusUnknown");
|
|
|
|
const modeLabel =
|
|
mode === "light"
|
|
? t("settings.appearance.light")
|
|
: mode === "dark"
|
|
? t("settings.appearance.dark")
|
|
: t("settings.appearance.system");
|
|
|
|
const langLabel =
|
|
lang === "en"
|
|
? t("settings.language.en")
|
|
: lang === "ar"
|
|
? t("settings.language.ar")
|
|
: t("settings.language.fr");
|
|
|
|
const callEmergency = useCallback(async () => {
|
|
try {
|
|
await Linking.openURL("tel:112");
|
|
} catch {
|
|
Alert.alert(
|
|
t("settings.safety.callFailedTitle"),
|
|
t("settings.safety.callFailedBody"),
|
|
);
|
|
}
|
|
}, [t]);
|
|
|
|
const chooseLanguage = useCallback(
|
|
(next: Lang) => {
|
|
const switchingToOrFromRTL = next === "ar" || lang === "ar";
|
|
|
|
setLang(next);
|
|
|
|
if (switchingToOrFromRTL) {
|
|
Alert.alert(
|
|
t("settings.language.rtlRestartTitle"),
|
|
t("settings.language.rtlRestartBody"),
|
|
);
|
|
}
|
|
},
|
|
[lang, setLang, t],
|
|
);
|
|
|
|
const openOverlaySettings = useCallback(async () => {
|
|
setOverlayRequested(true);
|
|
try {
|
|
await Linking.openSettings();
|
|
} catch {
|
|
// already flagged; nothing more to do
|
|
}
|
|
}, [setOverlayRequested]);
|
|
|
|
const appearanceOptions: { mode: ThemeMode; icon: IconName }[] = [
|
|
{ mode: "light", icon: "white-balance-sunny" },
|
|
{ mode: "dark", icon: "weather-night" },
|
|
{ mode: "system", icon: "theme-light-dark" },
|
|
];
|
|
|
|
const languageOptions: { lang: Lang; icon: IconName }[] = [
|
|
{ lang: "en", icon: "alpha-e-box" },
|
|
{ lang: "ar", icon: "alpha-a-box" },
|
|
{ lang: "fr", icon: "alpha-f-box" },
|
|
];
|
|
|
|
const safetyTiles: { key: string; icon: IconName; title: string; body: string }[] = [
|
|
{
|
|
key: "proactive",
|
|
icon: "shield-account",
|
|
title: t("settings.safety.proactive.title"),
|
|
body: t("settings.safety.proactive.body"),
|
|
},
|
|
{
|
|
key: "verification",
|
|
icon: "account-check",
|
|
title: t("settings.safety.verification.title"),
|
|
body: t("settings.safety.verification.body"),
|
|
},
|
|
{
|
|
key: "privacy",
|
|
icon: "lock",
|
|
title: t("settings.safety.privacy.title"),
|
|
body: t("settings.safety.privacy.body"),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
|
|
<ScrollView
|
|
className="px-5"
|
|
contentContainerStyle={{ paddingBottom: 120 }}
|
|
>
|
|
<Text className="text-2xl font-JakartaBold my-5 text-black dark:text-white">
|
|
{t("settings.title")}
|
|
</Text>
|
|
|
|
{/* 1. Maps & Navigation */}
|
|
<SectionHeader title={t("settings.maps.title")} />
|
|
<Card>
|
|
<SettingsRow
|
|
icon="map-marker-radius"
|
|
title={t("settings.maps.title")}
|
|
subtitle={t("settings.maps.description")}
|
|
right="value"
|
|
value={locationStatusLabel}
|
|
/>
|
|
{status !== "granted" ? (
|
|
<View className="border-t border-neutral-100 dark:border-neutral-800">
|
|
<SettingsRow
|
|
icon="cog"
|
|
title={t("settings.maps.openSettings")}
|
|
right="chevron"
|
|
onPress={openSettings}
|
|
/>
|
|
</View>
|
|
) : null}
|
|
</Card>
|
|
|
|
{/* 2. Appearance */}
|
|
<SectionHeader title={t("settings.appearance.title")} />
|
|
<Card>
|
|
<View className="px-4 py-2.5">
|
|
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
|
|
{t("settings.appearance.description")}
|
|
</Text>
|
|
</View>
|
|
{appearanceOptions.map((option, index) => (
|
|
<View
|
|
key={option.mode}
|
|
className={
|
|
index > 0
|
|
? "border-t border-neutral-100 dark:border-neutral-800"
|
|
: ""
|
|
}
|
|
>
|
|
<SettingsRow
|
|
icon={option.icon}
|
|
title={
|
|
option.mode === "light"
|
|
? t("settings.appearance.light")
|
|
: option.mode === "dark"
|
|
? t("settings.appearance.dark")
|
|
: t("settings.appearance.system")
|
|
}
|
|
right="value"
|
|
value={
|
|
mode === option.mode
|
|
? isDark
|
|
? "✓"
|
|
: "✓"
|
|
: ""
|
|
}
|
|
onPress={() => setMode(option.mode)}
|
|
/>
|
|
</View>
|
|
))}
|
|
</Card>
|
|
|
|
{/* 3. Safety */}
|
|
<SectionHeader title={t("settings.safety.title")} />
|
|
<Card>
|
|
<SettingsRow
|
|
icon="phone-in-talk"
|
|
title={t("settings.safety.call112")}
|
|
subtitle={t("settings.safety.call112Description")}
|
|
right="chevron"
|
|
danger
|
|
onPress={callEmergency}
|
|
/>
|
|
{safetyTiles.map((tile) => (
|
|
<View
|
|
key={tile.key}
|
|
className="border-t border-neutral-100 dark:border-neutral-800"
|
|
>
|
|
<SettingsRow
|
|
icon={tile.icon}
|
|
title={tile.title}
|
|
subtitle={
|
|
expandedSafety === tile.key ? undefined : tile.body
|
|
}
|
|
right="chevron"
|
|
onPress={() =>
|
|
setExpandedSafety((current) =>
|
|
current === tile.key ? null : tile.key,
|
|
)
|
|
}
|
|
/>
|
|
</View>
|
|
))}
|
|
</Card>
|
|
|
|
{/* 4. Language */}
|
|
<SectionHeader title={t("settings.language.title")} />
|
|
<Card>
|
|
<View className="px-4 py-2.5">
|
|
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
|
|
{t("settings.language.description")}
|
|
</Text>
|
|
</View>
|
|
{languageOptions.map((option, index) => (
|
|
<View
|
|
key={option.lang}
|
|
className={
|
|
index > 0
|
|
? "border-t border-neutral-100 dark:border-neutral-800"
|
|
: ""
|
|
}
|
|
>
|
|
<SettingsRow
|
|
icon={option.icon}
|
|
title={
|
|
option.lang === "en"
|
|
? t("settings.language.en")
|
|
: option.lang === "ar"
|
|
? t("settings.language.ar")
|
|
: t("settings.language.fr")
|
|
}
|
|
right="value"
|
|
value={lang === option.lang ? "✓" : ""}
|
|
onPress={() => chooseLanguage(option.lang)}
|
|
/>
|
|
</View>
|
|
))}
|
|
</Card>
|
|
|
|
{/* 5. Keep awake */}
|
|
<SectionHeader title={t("settings.keepAwake.title")} />
|
|
<Card>
|
|
<SettingsRow
|
|
icon="monitor"
|
|
title={t("settings.keepAwake.title")}
|
|
subtitle={t("settings.keepAwake.description")}
|
|
right="switch"
|
|
switchValue={keepAwake}
|
|
onSwitchChange={setKeepAwake}
|
|
/>
|
|
</Card>
|
|
|
|
{/* 6. Display over other apps (Android only) */}
|
|
{Platform.OS === "android" ? (
|
|
<>
|
|
<SectionHeader title={t("settings.overlay.title")} />
|
|
<Card>
|
|
<SettingsRow
|
|
icon="application-brackets"
|
|
title={t("settings.overlay.allow")}
|
|
subtitle={
|
|
overlayRequested
|
|
? t("settings.overlay.openedHint")
|
|
: t("settings.overlay.description")
|
|
}
|
|
right="chevron"
|
|
onPress={openOverlaySettings}
|
|
/>
|
|
</Card>
|
|
</>
|
|
) : null}
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
export default Settings; |