Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/app-expo/src/navigation/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import AppearanceSettingsScreen from "@/screens/settings/AppearanceSettingsScree
import FeedbackDetailScreen from "@/screens/settings/FeedbackDetailScreen";
import FeedbackScreen from "@/screens/settings/FeedbackScreen";
import FontSettingsScreen from "@/screens/settings/FontSettingsScreen";
import ReadingSettingsScreen from "@/screens/settings/ReadingSettingsScreen";
import SyncSettingsScreen from "@/screens/settings/SyncSettingsScreen";
import TTSSettingsScreen from "@/screens/settings/TTSSettingsScreen";
import TranslationSettingsScreen from "@/screens/settings/TranslationSettingsScreen";
Expand All @@ -37,6 +38,7 @@ export type RootStackParamList = {
Skills: undefined;
VectorModelSettings: undefined;
AppearanceSettings: undefined;
ReadingSettings: undefined;
AISettings: undefined;
TTSSettings: undefined;
TranslationSettings: undefined;
Expand Down Expand Up @@ -102,6 +104,7 @@ export function RootNavigator() {
options={{ animation: "slide_from_right" }}
/>
<Stack.Screen name="AppearanceSettings" component={AppearanceSettingsScreen} />
<Stack.Screen name="ReadingSettings" component={ReadingSettingsScreen} />
<Stack.Screen name="AISettings" component={AISettingsScreen} />
<Stack.Screen name="TTSSettings" component={TTSSettingsScreen} />
<Stack.Screen name="TranslationSettings" component={TranslationSettingsScreen} />
Expand Down
6 changes: 6 additions & 0 deletions packages/app-expo/src/screens/ProfileScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type ProfileMenuIcon = (props: {
type ProfileMenuRoute = Extract<
keyof RootStackParamList,
| "AppearanceSettings"
| "ReadingSettings"
| "FontSettings"
| "SyncSettings"
| "AISettings"
Expand Down Expand Up @@ -429,6 +430,11 @@ export function ProfileScreen() {
label: t("settings.general", "通用"),
route: "AppearanceSettings" as const,
},
{
icon: BookOpenIcon,
label: t("settings.reading_title", "Reading"),
route: "ReadingSettings" as const,
},
{
icon: TypeIcon,
label: t("fonts.title", "字体"),
Expand Down
97 changes: 72 additions & 25 deletions packages/app-expo/src/screens/reader/ReaderSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,62 @@ import { XIcon } from "@/components/ui/Icon";
import { useResponsiveLayout } from "@/hooks/use-responsive-layout";
import { useColors } from "@/styles/theme";
import type { ReadSettings } from "@readany/core/types";
import { ActivityIndicator, Modal, Platform, Pressable, ScrollView, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { type ReactNode, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Modal,
Platform,
Pressable,
ScrollView,
Text,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { makeStyles } from "./reader-styles";
import { useFontStore } from "@readany/core/stores";
import { useRubyStore, type RubyMode } from "@readany/core/stores/ruby-store";
import { useCallback, useState } from "react";

interface Props {
visible: boolean;
readSettings: ReadSettings;
bookId?: string;
embedded?: boolean;
onClose: () => void;
onUpdateSetting: <K extends keyof ReadSettings>(key: K, value: ReadSettings[K]) => void;
onRubyModeChange?: (mode: RubyMode) => void;
}

export function ReaderSettingsPanel({ visible, readSettings, bookId, onClose, onUpdateSetting, onRubyModeChange }: Props) {
function ReaderSettingsContainer({
children,
embedded,
visible,
onClose,
}: {
children: ReactNode;
embedded: boolean;
visible: boolean;
onClose: () => void;
}) {
if (embedded) return <View style={{ flex: 1 }}>{children}</View>;

return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
{children}
</Modal>
);
}

export function ReaderSettingsPanel({
visible,
readSettings,
bookId,
embedded = false,
onClose,
onUpdateSetting,
onRubyModeChange,
}: Props) {
const colors = useColors();
const s = makeStyles(colors);
const insets = useSafeAreaInsets();
Expand All @@ -47,28 +85,37 @@ export function ReaderSettingsPanel({ visible, readSettings, bookId, onClose, on
} = readSettings;

return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={s.modalBackdrop} onPress={onClose} />
<ReaderSettingsContainer embedded={embedded} visible={visible} onClose={onClose}>
{!embedded && <Pressable style={s.modalBackdrop} onPress={onClose} />}
<View
style={[
s.bottomSheet,
{ paddingBottom: insets.bottom || 16 },
layout.isTablet && {
width: "100%",
},
]}
style={
embedded
? {
flex: 1,
width: "100%",
maxWidth: layout.centeredContentWidth,
alignSelf: "center",
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: insets.bottom || 16,
}
: [
s.bottomSheet,
{ paddingBottom: insets.bottom || 16 },
layout.isTablet && {
width: "100%",
},
]
}
>
<View style={s.sheetHeader}>
<Text style={s.sheetTitle}>{t("reader.settings", "阅读设置")}</Text>
<TouchableOpacity onPress={onClose}>
<XIcon size={18} color={colors.mutedForeground} />
</TouchableOpacity>
</View>
{!embedded && (
<View style={s.sheetHeader}>
<Text style={s.sheetTitle}>{t("reader.settings", "阅读设置")}</Text>
<TouchableOpacity onPress={onClose}>
<XIcon size={18} color={colors.mutedForeground} />
</TouchableOpacity>
</View>
)}
<ScrollView showsVerticalScrollIndicator={false}>
{/* Font Size */}
<View style={s.settingRow}>
Expand Down Expand Up @@ -351,7 +398,7 @@ export function ReaderSettingsPanel({ visible, readSettings, bookId, onClose, on
)}
</ScrollView>
</View>
</Modal>
</ReaderSettingsContainer>
);
}

Expand Down
38 changes: 38 additions & 0 deletions packages/app-expo/src/screens/settings/ReadingSettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReaderSettingsPanel } from "@/screens/reader/ReaderSettingsPanel";
import { useSettingsStore } from "@/stores";
import { useColors } from "@/styles/theme";
import type { ReadSettings } from "@readany/core/types";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { SafeAreaView } from "react-native-safe-area-context";
import { SettingsHeader } from "./SettingsHeader";

export default function ReadingSettingsScreen() {
const { t } = useTranslation();
const colors = useColors();
const readSettings = useSettingsStore((state) => state.readSettings);
const updateReadSettings = useSettingsStore((state) => state.updateReadSettings);

const updateSetting = useCallback(
<K extends keyof ReadSettings>(key: K, value: ReadSettings[K]) => {
updateReadSettings({ [key]: value } as Partial<ReadSettings>);
},
[updateReadSettings],
);

return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={["top"]}>
<SettingsHeader
title={t("settings.reading_title", "Reading")}
subtitle={t("settings.realtimeHint")}
/>
<ReaderSettingsPanel
embedded
visible
readSettings={readSettings}
onClose={() => undefined}
onUpdateSetting={updateSetting}
/>
</SafeAreaView>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

const root = resolve(import.meta.dirname, "../../../../..");
const read = (path: string) => readFileSync(resolve(root, path), "utf8");

describe("Reading settings navigation", () => {
it("exposes the shared reader settings from Me", () => {
const profile = read("packages/app-expo/src/screens/ProfileScreen.tsx");
const navigator = read("packages/app-expo/src/navigation/RootNavigator.tsx");
const screen = read("packages/app-expo/src/screens/settings/ReadingSettingsScreen.tsx");

expect(profile).toContain('route: "ReadingSettings"');
expect(navigator).toContain("ReadingSettings: undefined");
expect(navigator).toContain('name="ReadingSettings"');
expect(screen).toContain("<ReaderSettingsPanel");
expect(screen).toContain("embedded");
expect(screen).toContain("updateReadSettings({ [key]: value }");
});
});